diff --git a/agent/agent.go b/agent/agent.go
index b7472f40..9b819486 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -407,7 +407,11 @@ form.addEventListener('submit',function(e){
.then(function(resp){
if(!resp.ok&&resp.status===401){
prog.style.display='none';
- result.innerHTML='
Please login to use the agent.
';
+ resp.json().then(function(j){
+ result.innerHTML='';
+ }).catch(function(){
+ result.innerHTML='Please login to use the agent.
';
+ });
btn.disabled=false;btn.textContent='Do';
return;
}
@@ -751,11 +755,18 @@ func handleQuery(w http.ResponseWriter, r *http.Request) {
return
}
- // Require authentication
- _, acc, err := auth.RequireSession(r)
- if err != nil {
- http.Error(w, `{"error":"authentication required"}`, http.StatusUnauthorized)
- return
+ _, acc := auth.TrySession(r)
+ isGuest := acc == nil
+
+ if isGuest {
+ ip := app.ClientIP(r)
+ if !guestQueryAllowed(ip) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ w.Write([]byte(`{"error":"Sign up to keep using the AI agent. 3 free queries per day."}`))
+ return
+ }
+ guestQueryRecord(ip)
}
// Resolve model
@@ -767,8 +778,8 @@ func handleQuery(w http.ResponseWriter, r *http.Request) {
}
}
- // Check wallet quota before starting
- if QuotaCheck != nil {
+ // Check wallet quota (authenticated users only)
+ if !isGuest && QuotaCheck != nil {
canProceed, _, err := QuotaCheck(r, model.WalletOp)
if !canProceed {
msg := "Insufficient credits for agent query. Top up at /wallet."
@@ -788,16 +799,22 @@ func handleQuery(w http.ResponseWriter, r *http.Request) {
// Create flow early so progress is saved server-side even if the
// SSE connection drops (e.g. mobile browser suspends the tab).
+ accountID := ""
+ if acc != nil {
+ accountID = acc.ID
+ }
flow := &Flow{
ID: newFlowID(),
- AccountID: acc.ID,
+ AccountID: accountID,
Prompt: req.Prompt,
Status: "running",
ParentID: req.ContextID,
CreatedAt: time.Now().UTC(),
}
- if err := saveFlow(flow); err != nil {
- app.Log("agent", "Failed to create flow: %v", err)
+ if !isGuest {
+ if err := saveFlow(flow); err != nil {
+ app.Log("agent", "Failed to create flow: %v", err)
+ }
}
// Start SSE stream
diff --git a/agent/guest.go b/agent/guest.go
new file mode 100644
index 00000000..c533a1e3
--- /dev/null
+++ b/agent/guest.go
@@ -0,0 +1,44 @@
+package agent
+
+import (
+ "sync"
+ "time"
+)
+
+const guestDailyLimit = 3
+
+var (
+ guestMu sync.Mutex
+ guestCounts = map[string]*guestBucket{}
+)
+
+type guestBucket struct {
+ count int
+ resetAt time.Time
+}
+
+func guestQueryAllowed(ip string) bool {
+ guestMu.Lock()
+ defer guestMu.Unlock()
+
+ b, ok := guestCounts[ip]
+ if !ok || time.Now().After(b.resetAt) {
+ return true
+ }
+ return b.count < guestDailyLimit
+}
+
+func guestQueryRecord(ip string) {
+ guestMu.Lock()
+ defer guestMu.Unlock()
+
+ b, ok := guestCounts[ip]
+ if !ok || time.Now().After(b.resetAt) {
+ guestCounts[ip] = &guestBucket{
+ count: 1,
+ resetAt: time.Now().Add(24 * time.Hour),
+ }
+ return
+ }
+ b.count++
+}
diff --git a/agent/run.go b/agent/run.go
index 726273f1..9134b4d7 100644
--- a/agent/run.go
+++ b/agent/run.go
@@ -102,14 +102,20 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
return
}
- _, acc, err := auth.RequireSession(r)
- if err != nil {
- w.WriteHeader(401)
- app.RespondJSON(w, RunResponse{Error: "authentication required"})
- return
+ _, acc := auth.TrySession(r)
+ isGuest := acc == nil
+
+ if isGuest {
+ ip := app.ClientIP(r)
+ if !guestQueryAllowed(ip) {
+ w.WriteHeader(401)
+ app.RespondJSON(w, RunResponse{Error: "Sign up to keep using the AI agent. 3 free queries per day."})
+ return
+ }
+ guestQueryRecord(ip)
}
- // Check quota
+ // Check quota (authenticated users only)
model := Models[0]
for _, m := range Models {
if m.ID == req.Model {
@@ -117,7 +123,7 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
break
}
}
- if QuotaCheck != nil {
+ if !isGuest && QuotaCheck != nil {
canProceed, _, err := QuotaCheck(r, model.WalletOp)
if !canProceed {
w.WriteHeader(402)
@@ -130,11 +136,9 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
}
}
- _ = acc // authenticated
-
// Step 1: Plan
userCtx := ""
- if UserContextFunc != nil {
+ if !isGuest && UserContextFunc != nil {
userCtx = UserContextFunc(acc.ID)
}
planSystem := "You are an AI agent. Given a user question, output ONLY a JSON array of tool calls.\n\n" +
@@ -217,10 +221,13 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
answer = app.StripLatexDollars(answer)
- // Check if the user asked to remember something.
+ if isGuest {
+ app.RespondJSON(w, RunResponse{Answer: answer, Tools: toolsUsed})
+ return
+ }
+
go extractMemory(acc.ID, req.Prompt)
- // Save as a flow so it appears in the agent history at /agent.
var steps []FlowStep
for _, tu := range toolsUsed {
steps = append(steps, FlowStep{Tool: tu.Name})
diff --git a/home/guest.go b/home/guest.go
new file mode 100644
index 00000000..9a39be69
--- /dev/null
+++ b/home/guest.go
@@ -0,0 +1,68 @@
+package home
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "mu/internal/app"
+)
+
+func serveGuestHome(w http.ResponseWriter, r *http.Request) {
+ RefreshCards()
+
+ var b strings.Builder
+
+ // AI prompt — guest trial (3 free queries tracked by IP)
+ b.WriteString(``)
+ b.WriteString(`
Your personal AI
`)
+ b.WriteString(`
Ask anything. Try it free.
`)
+ b.WriteString(`
`)
+
+ b.WriteString(``)
+ b.WriteString(`
`)
+
+ // Suggestion pills
+ suggestions := []string{"Today's news", "Bitcoin price", "What is Mu?"}
+ b.WriteString(`
`)
+ for _, s := range suggestions {
+ b.WriteString(fmt.Sprintf(`
%s`, htmlEsc(url.QueryEscape(s)), htmlEsc(s)))
+ }
+ b.WriteString(`
`)
+ b.WriteString(`
`)
+
+ // Public content cards — show a taste of what's available
+ b.WriteString(``)
+
+ // News preview
+ cacheMutex.RLock()
+ for _, card := range Cards {
+ if card.ID == "news" || card.ID == "markets" || card.ID == "blog" || card.ID == "reminder" {
+ content := card.CachedHTML
+ if strings.TrimSpace(content) == "" {
+ continue
+ }
+ if card.Link != "" {
+ content += app.Link("More", card.Link)
+ }
+ b.WriteString(fmt.Sprintf(app.CardTemplate, card.ID, card.ID, card.Title, content))
+ }
+ }
+ cacheMutex.RUnlock()
+
+ b.WriteString(`
`)
+
+ // CTA
+ b.WriteString(``)
+ b.WriteString(`
Get the full experience — AI agent with memory, mail, web search, and more.
`)
+ b.WriteString(`
View pricing`)
+ b.WriteString(`
Sign up`)
+ b.WriteString(`
`)
+
+ html := app.RenderHTMLWithLangAndBody("Mu — Your Personal AI", "News, mail, markets, search and more through one AI", b.String(), "en", ` class="page-home"`)
+ w.Write([]byte(html))
+}
diff --git a/home/home.go b/home/home.go
index a22e917c..19763e75 100644
--- a/home/home.go
+++ b/home/home.go
@@ -370,6 +370,13 @@ func Handler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Guest landing page for unauthenticated visitors.
+ _, viewerAcc := auth.TrySession(r)
+ if viewerAcc == nil {
+ serveGuestHome(w, r)
+ return
+ }
+
// Refresh cards if cache expired (2 minute TTL)
RefreshCards()
@@ -378,7 +385,6 @@ func Handler(w http.ResponseWriter, r *http.Request) {
// Date header + weather + invite — Overview only, built here
// and injected into the cards div below.
now := time.Now()
- _, viewerAcc := auth.TrySession(r)
var dateLine strings.Builder
inviteHTML := ""
if viewerAcc != nil {
diff --git a/home/pricing.go b/home/pricing.go
new file mode 100644
index 00000000..ba9e7827
--- /dev/null
+++ b/home/pricing.go
@@ -0,0 +1,93 @@
+package home
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "mu/internal/app"
+ "mu/wallet"
+)
+
+func PricingHandler(w http.ResponseWriter, r *http.Request) {
+ var b strings.Builder
+
+ // Hero
+ b.WriteString(``)
+ b.WriteString(`
Your personal AI
`)
+ b.WriteString(`
News, mail, markets, search, weather, and more — all through one AI that knows what you care about.
`)
+ b.WriteString(`
`)
+
+ // Plans
+ b.WriteString(``)
+
+ // Starter plan
+ b.WriteString(`
`)
+ b.WriteString(`
Starter
`)
+ b.WriteString(`
£5/month
`)
+ b.WriteString(`
500 credits
`)
+ b.WriteString(`
`)
+ b.WriteString(`- ✓ AI agent with memory
`)
+ b.WriteString(`- ✓ News, markets, weather
`)
+ b.WriteString(`- ✓ Mail and messaging
`)
+ b.WriteString(`- ✓ Web search
`)
+ b.WriteString(`- ✓ Build apps with AI
`)
+ b.WriteString(`
`)
+ b.WriteString(`
Get started`)
+ b.WriteString(`
`)
+
+ // Pro plan
+ b.WriteString(`
`)
+ b.WriteString(`
Pro
`)
+ b.WriteString(`
£10/month
`)
+ b.WriteString(`
1,200 credits
`)
+ b.WriteString(`
`)
+ b.WriteString(`- ✓ Everything in Starter
`)
+ b.WriteString(`- ✓ More credits per month
`)
+ b.WriteString(`- ✓ Priority AI models
`)
+ b.WriteString(`
`)
+ b.WriteString(`
Get started`)
+ b.WriteString(`
`)
+
+ b.WriteString(`
`)
+
+ // What's included (free)
+ b.WriteString(``)
+ b.WriteString(`
Included for everyone
`)
+ b.WriteString(`
Browse without an account. No ads, no tracking.
`)
+ b.WriteString(`
`)
+ b.WriteString(`- ✓ News headlines and feeds
`)
+ b.WriteString(`- ✓ Market prices
`)
+ b.WriteString(`- ✓ Blog posts and social
`)
+ b.WriteString(`- ✓ Video
`)
+ b.WriteString(`- ✓ 3 free AI questions as a guest
`)
+ b.WriteString(`
`)
+ b.WriteString(`
`)
+
+ // Credit costs
+ b.WriteString(``)
+ b.WriteString(`
Credit costs
`)
+ b.WriteString(`
1 credit = 1p. Pay for what you use.
`)
+ b.WriteString(`
`)
+ b.WriteString(`| AI agent | ` + fmt.Sprintf("%d", wallet.CostAgentQuery) + ` |
`)
+ b.WriteString(`| Chat | ` + fmt.Sprintf("%d", wallet.CostChatQuery) + ` |
`)
+ b.WriteString(`| Web search | ` + fmt.Sprintf("%d", wallet.CostWebSearch) + ` |
`)
+ b.WriteString(`| Weather | ` + fmt.Sprintf("%d", wallet.CostWeatherForecast) + ` |
`)
+ b.WriteString(`| Mail | ` + fmt.Sprintf("%d", wallet.CostMailSend) + ` |
`)
+ b.WriteString(`| News search | ` + fmt.Sprintf("%d", wallet.CostNewsSearch) + ` |
`)
+ b.WriteString(`
`)
+ b.WriteString(`
`)
+
+ // Self-host
+ b.WriteString(``)
+ b.WriteString(`
Self-host
`)
+ b.WriteString(`
Run your own instance with no limits. Single Go binary, bring your own AI provider.
`)
+ b.WriteString(`
github.com/micro/mu
`)
+ b.WriteString(`
`)
+
+ // Login link
+ b.WriteString(`Already have an account? Log in
`)
+
+ html := app.RenderHTML("Pricing", "Personal AI — plans and pricing", b.String())
+ w.Write([]byte(html))
+}
diff --git a/main.go b/main.go
index 48435e83..712aa5b4 100644
--- a/main.go
+++ b/main.go
@@ -715,6 +715,7 @@ func main() {
"/web/read": false, // Public page, auth checked in handler (proxied reader)
"/status": false, // Public - server health status
+ "/pricing": false, // Public - pricing page
"/docs": false, // Public - documentation
"/whitepaper": false, // Public - whitepaper
"/mcp": false, // Public - MCP tools page
@@ -829,6 +830,7 @@ func main() {
// serve the home screen
http.HandleFunc("/home", home.Handler)
+ http.HandleFunc("/pricing", home.PricingHandler)
// serve the agent
http.HandleFunc("/agent", agent.Handler)