Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ See [API docs](https://mu.xyz/api) · [MCP docs](docs/MCP.md)

## Pricing

Browsing is included. AI features use credits — 100/day with every account, then pay as you go from 1p per query.
Browsing is included. AI and search features use credits — 1 credit = 1p, pay as you go.

- **Card** — Top up via Stripe. 1 credit = 1p.
- **Crypto** — AI agents pay per-request with USDC via [x402](https://x402.org). No account needed.
Expand Down
3 changes: 1 addition & 2 deletions admin/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,9 @@ func runCommand(cmd string) string {
return "usage: wallet <user_id>"
}
w := wallet.GetWallet(arg(1))
usage := wallet.GetDailyUsage(arg(1))
txns := wallet.GetTransactions(arg(1), 10)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Balance: %d credits\nDaily usage: %d / %d\n", w.Balance, usage.Used, wallet.DailyQuota))
sb.WriteString(fmt.Sprintf("Balance: %d credits\n", w.Balance))
if len(txns) > 0 {
sb.WriteString("\nRecent transactions:\n")
for _, tx := range txns {
Expand Down
4 changes: 1 addition & 3 deletions docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@ export DONATION_URL="https://gocardless.com/your-donation-link"
## Quota Configuration

```bash
# Daily AI queries per account (default: 10)
export DAILY_QUOTA="100"
# Credit costs per operation (defaults shown)

# Credit costs per operation (default values shown)
export CREDIT_COST_NEWS="1" # News search (1p)
Expand Down Expand Up @@ -190,7 +189,6 @@ export MAIL_SELECTOR="default"
| `X402_ASSETS` | `USDC,EURC` | Accepted tokens (comma-separated symbols) |
| `X402_FACILITATOR_URL` | `https://x402.org/facilitator` | x402 facilitator endpoint |
| `X402_NETWORK` | `eip155:8453` | Blockchain network for x402 payments |
| `DAILY_QUOTA` | `100` | Daily AI queries per account |
| `CREDIT_COST_NEWS` | `1` | Credits per news search |
| `CREDIT_COST_VIDEO` | `2` | Credits per video search |
| `CREDIT_COST_VIDEO_WATCH` | `0` | Credits per video watch (free by default) |
Expand Down
4 changes: 2 additions & 2 deletions docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Both return a session token. Use it in subsequent requests:
Authorization: Bearer SESSION_TOKEN
```

Every account includes **100 credits per day** and can top up with a card via Stripe.
Accounts can top up credits with a card via Stripe.

## Available Tools

Expand Down Expand Up @@ -210,7 +210,7 @@ curl -X POST https://mu.xyz/mcp \
| Auth header | `X-PAYMENT` | `Authorization: Bearer` |
| Payment model | Per request | Pre-paid credits |
| Currency | USDC | GBP |
| Daily allowance | No | 100 queries/day |
| Billing | Per request | Pre-paid credits |
| Best for | Autonomous agents | Human users, MCP clients |

## Self-Hosting
Expand Down
2 changes: 1 addition & 1 deletion docs/SYSTEM_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Live financial data.

Credit-based usage metering.

- **Pay as you go** — 100 credits/day included, then 1 credit = 1p
- **Pay as you go** — 1 credit = 1p, browsing included
- **Stripe payments** - Card top-up
- **Quota enforcement** - Integrated with API and agent
- **Transaction tracking** - Usage history
Expand Down
3 changes: 1 addition & 2 deletions docs/VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ AI agents can pay per-request with USDC through the [x402 protocol](https://x402
## Pricing

- **Browsing included** — news, blogs, videos, markets
- **100 credits per day** — covers search, chat, and AI features
- **Pay as you go** — 1 credit = 1p, top up via card
- **Pay as you go** — AI and search use credits, 1 credit = 1p
- **Crypto** — AI agents pay per-request via [x402](https://x402.org)
- **Self-host** — run your own instance, no restrictions

Expand Down
42 changes: 10 additions & 32 deletions docs/WALLET_AND_CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ Mu is a tool, not a destination. Like Google Search in 2000 — you arrive with

Credits are a straightforward way to pay for what you use. No dark patterns, no pressure to upgrade, no "unlimited" tiers that incentivize us to maximize your engagement.

- **Daily allowance**: 100 AI queries/day — enough for daily use
- **Pay-as-you-go**: Top up with a card or pay per-request with crypto
- **Self-host**: Run your own instance for free, forever
- **Pay as you go**: Top up with a card or pay per-request with crypto
- **Self-host**: Run your own instance, no restrictions

We charge because LLMs and APIs cost money. Here's our actual cost breakdown — we're not extracting margin, just covering infrastructure.

Expand All @@ -21,15 +20,6 @@ We charge because LLMs and APIs cost money. Here's our actual cost breakdown —
- Top up via card payment (Stripe) or pay per-request with crypto (x402)
- Credits never expire

### Daily Allowance

Every account includes **100 credits per day**:
- Resets at midnight UTC
- Covers news search, video search, and chat AI queries
- No payment required

This should be enough if you're using Mu as a utility. If you need more, pay-as-you-go.

### Credit Costs

| Feature | Cost | Why |
Expand All @@ -50,11 +40,11 @@ This should be enough if you're using Mu as a utility. If you need more, pay-as-

### Who Pays What

| User Type | Daily Allowance | Credits | Notes |
|-----------|------------|---------|-------|
| Guest | 0 | N/A | Must register |
| Registered | 100 credits | Pay-as-you-go | When daily allowance used |
| Admin | Unlimited | Not needed | Site administrators |
| User Type | Credits | Notes |
|-----------|---------|-------|
| Guest | N/A | Must register |
| Registered | Pay as you go | Top up via card or crypto |
| Admin | Unlimited | Site administrators |

## Why No "Unlimited" Tier?

Expand Down Expand Up @@ -175,16 +165,6 @@ type Transaction struct {
}
```

### Daily Usage

```go
type DailyUsage struct {
UserID string `json:"user_id"`
Date string `json:"date"` // "2006-01-02"
Used int `json:"used"` // Quota used today
}
```

---

## API Endpoints
Expand Down Expand Up @@ -215,8 +195,7 @@ X402_FACILITATOR_URL="https://x402.org/facilitator"
X402_NETWORK="eip155:8453"
X402_ASSET="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"

# Quota (optional - these are defaults)
DAILY_QUOTA="100"
# Credit costs (optional - these are defaults)
CREDIT_COST_NEWS="1"
CREDIT_COST_NEWS_SUMMARY="1"
CREDIT_COST_VIDEO="2"
Expand All @@ -238,9 +217,8 @@ CREDIT_COST_WEATHER_POLLEN="1"
1. User initiates search/chat
2. Check for x402 payment header → verify and settle on-chain (no account needed)
3. Check if admin → allow (no charge)
4. Check daily quota → allow if available, decrement
5. Check wallet balance → allow if sufficient, deduct credits
6. Otherwise → return 402 with payment requirements (if x402 enabled) or show "quota exceeded"
4. Check wallet balance → allow if sufficient, deduct credits
5. Otherwise → return 402 with payment requirements (if x402 enabled) or show "credits required"

---

Expand Down
6 changes: 2 additions & 4 deletions docs/WHITEPAPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ Each service does one thing. There are no social feeds combining heterogeneous c

The revenue model determines platform behaviour. Advertising-funded platforms are structurally incentivised to maximise attention. Subscription platforms are incentivised to maximise perceived value, which leads to feature bloat and engagement optimisation.

Mu uses per-use micropayments. The platform is incentivised to build tools that solve the user's problem as quickly as possible — the opposite of engagement maximisation. Browsing is included. Only operations that consume infrastructure (API calls, LLM inference, SMTP delivery) carry a cost. Every account includes a daily quota for casual use.

Operations that create public content or reach external systems (email, blog posts, web fetching) always require real credits, even within the daily quota. This prevents abuse at scale.
Mu uses per-use micropayments. The platform is incentivised to build tools that solve the user's problem as quickly as possible — the opposite of engagement maximisation. Browsing is included. Only operations that consume infrastructure (API calls, LLM inference, SMTP delivery) carry a cost. Users pay for what they use, nothing more.

### 2.4 Self-Hosting

Expand Down Expand Up @@ -138,7 +136,7 @@ Read-only operations — browsing news feeds, reading blog posts, watching video

### 3.3 Daily Quota

Each account includes a daily allocation of one hundred queries, resetting at midnight UTC. This quota is sufficient for casual utility use. When the daily quota is exhausted, subsequent operations consume credits from the user's wallet. This model ensures accessibility while covering infrastructure costs for heavy usage.
All metered operations consume credits from the user's wallet. Browsing (news, blogs, videos, markets) is included at no cost. Operations that require infrastructure — AI inference, web search, email delivery — cost credits. This model ensures sustainability while keeping the platform accessible.

### 3.4 Incentive Alignment

Expand Down
16 changes: 6 additions & 10 deletions places/places.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
}

// Check quota
canProceed, useFree, cost, _ := wallet.CheckQuota(acc.ID, wallet.OpPlacesSearch)
canProceed, _, cost, _ := wallet.CheckQuota(acc.ID, wallet.OpPlacesSearch)
if !canProceed {
if app.WantsJSON(r) {
app.RespondError(w, http.StatusPaymentRequired, "Insufficient credits. Top up your wallet to continue.")
Expand Down Expand Up @@ -464,10 +464,8 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
sortBy := formValue("sort")
sortPlaces(results, sortBy)

// Consume quota after successful operation
if useFree {
wallet.UseQuota(acc.ID)
} else if cost > 0 {
// Deduct credits
if cost > 0 {
wallet.DeductCredits(acc.ID, cost, wallet.OpPlacesSearch, map[string]interface{}{"query": query})
}

Expand Down Expand Up @@ -516,7 +514,7 @@ func handleNearby(w http.ResponseWriter, r *http.Request) {
}

// Check quota
canProceed, useFree, cost, _ := wallet.CheckQuota(acc.ID, wallet.OpPlacesNearby)
canProceed, _, cost, _ := wallet.CheckQuota(acc.ID, wallet.OpPlacesNearby)
if !canProceed {
if app.WantsJSON(r) {
app.RespondError(w, http.StatusPaymentRequired, "Insufficient credits. Top up your wallet to continue.")
Expand Down Expand Up @@ -583,10 +581,8 @@ func handleNearby(w http.ResponseWriter, r *http.Request) {
sortBy := formValue("sort")
sortPlaces(results, sortBy)

// Consume quota after successful operation
if useFree {
wallet.UseQuota(acc.ID)
} else if cost > 0 {
// Deduct credits
if cost > 0 {
wallet.DeductCredits(acc.ID, cost, wallet.OpPlacesNearby, map[string]interface{}{
"lat": lat, "lon": lon, "radius": radius,
})
Expand Down
34 changes: 7 additions & 27 deletions wallet/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import (
// WalletPage renders the wallet page HTML
func WalletPage(userID string) string {
wallet := GetWallet(userID)
usage := GetDailyUsage(userID)
freeRemaining := GetQuotaRemaining(userID)
transactions := GetTransactions(userID, 20)

// Check if user is admin
Expand All @@ -41,19 +39,6 @@ func WalletPage(userID string) string {
sb.WriteString(`</div>`)

if !isAdmin {
// Daily quota
sb.WriteString(`<div class="card">`)
sb.WriteString(`<h3>Daily Queries</h3>`)
usedPct := float64(usage.Used) / float64(DailyQuota) * 100
if usedPct > 100 {
usedPct = 100
}
sb.WriteString(`<div class="progress">`)
sb.WriteString(fmt.Sprintf(`<div class="progress-bar" style="width: %.0f%%;"></div>`, usedPct))
sb.WriteString(`</div>`)
sb.WriteString(fmt.Sprintf(`<p class="text-sm text-muted">%d of %d remaining · Resets midnight UTC</p>`, freeRemaining, DailyQuota))
sb.WriteString(`</div>`)

// Self-hosting note
sb.WriteString(`<div class="card">`)
sb.WriteString(`<h3>Self-Host</h3>`)
Expand Down Expand Up @@ -140,14 +125,10 @@ func QuotaExceededPage(operation string, cost int) string {
var sb strings.Builder

sb.WriteString(`<div class="card center-card-md">`)
sb.WriteString(`<h2>Daily Limit Reached</h2>`)
sb.WriteString(`<p>You've used your daily queries.</p>`)
sb.WriteString(`<h3 class="mt-5">Options</h3>`)
sb.WriteString(`<ul class="options-list">`)
sb.WriteString(`<li>Wait until midnight UTC for your quota to reset</li>`)
sb.WriteString(fmt.Sprintf(`<li><a href="/wallet">Use credits</a> (%d credit%s for this)</li>`, cost, pluralize(cost)))
sb.WriteString(`<li><a href="/wallet/topup">Add credits</a></li>`)
sb.WriteString(`</ul>`)
sb.WriteString(`<h2>Credits Required</h2>`)
sb.WriteString(fmt.Sprintf(`<p>This costs %d credit%s. `, cost, pluralize(cost)))
sb.WriteString(`<a href="/wallet/topup">Add credits</a> to continue.</p>`)
sb.WriteString(`<p class="text-sm text-muted">1 credit = 1p · <a href="/wallet">View wallet</a></p>`)
sb.WriteString(`</div>`)

return sb.String()
Expand Down Expand Up @@ -230,7 +211,7 @@ func PublicWalletPage() string {
// Intro
sb.WriteString(`<div class="card">`)
sb.WriteString(`<h3>Credits &amp; Pricing</h3>`)
sb.WriteString(`<p>Every account includes ` + fmt.Sprintf("%d", DailyQuota) + ` queries/day. Need more? Top up and pay as you go — no subscription required.</p>`)
sb.WriteString(`<p>Browsing is included. AI and search features use credits. Top up and pay as you go — no subscription required.</p>`)
sb.WriteString(`<p><a href="/login" class="btn">Login to view your balance</a>&nbsp;<a href="/signup" class="btn btn-secondary">Sign up</a></p>`)
sb.WriteString(`</div>`)

Expand Down Expand Up @@ -630,16 +611,15 @@ func handlePricing(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"currency": "GBP",
"credit_value": "£0.01",
"daily_allowance": DailyQuota,
"operations": items,
"operations": items,
})
return
}

var sb strings.Builder
sb.WriteString(`<div class="max-w-xl"><div class="card">`)
sb.WriteString(`<h3>Pricing</h3>`)
sb.WriteString(`<p class="info">1 credit = £0.01. Daily allowance: ` + fmt.Sprintf("%d", DailyQuota) + ` credits.</p>`)
sb.WriteString(`<p class="info">1 credit = £0.01. Browsing included. AI and search use credits.</p>`)
sb.WriteString(`<table class="stats-table">`)
sb.WriteString(`<tr><td>News, blogs, videos</td><td>included</td></tr>`)
for _, item := range items {
Expand Down
23 changes: 1 addition & 22 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,20 +458,9 @@ func GetOperationCost(operation string) int {
}
}

// paidOnly lists operations that cannot use the daily quota
// and always require real credits. These are actions with spam or
// abuse potential: sending messages, publishing content, or using
// the server as a proxy to fetch external URLs.
func paidOnly(operation string) bool {
switch operation {
case OpMailSend, OpExternalEmail, OpBlogCreate, OpWebFetch:
return true
}
return false
}

// CheckQuota checks if a user can perform an operation
// Returns: canProceed, useQuota, creditCost, error
// Returns: canProceed, useQuota (always false now), creditCost, error
func CheckQuota(userID string, operation string) (bool, bool, int, error) {
// Get account to check admin status
acc, err := auth.GetAccount(userID)
Expand All @@ -491,11 +480,6 @@ func CheckQuota(userID string, operation string) (bool, bool, int, error) {

cost := GetOperationCost(operation)

// Some operations always require real credits (e.g. email)
if !paidOnly(operation) && HasQuota(userID) {
return true, true, 0, nil
}

// Check if user has sufficient credits
balance := GetBalance(userID)
if balance >= cost {
Expand Down Expand Up @@ -549,11 +533,6 @@ func ConsumeQuota(userID string, operation string) error {
return nil
}

// Try daily quota first
if HasQuota(userID) {
return UseQuota(userID)
}

// Deduct credits
cost := GetOperationCost(operation)
return DeductCredits(userID, cost, operation, nil)
Expand Down
16 changes: 6 additions & 10 deletions weather/weather.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ func handleJSON(w http.ResponseWriter, r *http.Request) {

includePollen := r.URL.Query().Get("pollen") == "1"

// Check quota for weather forecast
canProceed, useFree, cost, _ := wallet.CheckQuota(acc.ID, wallet.OpWeatherForecast)
// Check credits
canProceed, _, cost, _ := wallet.CheckQuota(acc.ID, wallet.OpWeatherForecast)
if !canProceed {
app.RespondError(w, http.StatusPaymentRequired, "Insufficient credits. Top up your wallet to continue.")
return
Expand All @@ -126,10 +126,8 @@ func handleJSON(w http.ResponseWriter, r *http.Request) {
return
}

// Consume weather quota
if useFree {
wallet.UseQuota(acc.ID)
} else if cost > 0 {
// Deduct credits
if cost > 0 {
wallet.DeductCredits(acc.ID, cost, wallet.OpWeatherForecast, nil)
}

Expand All @@ -139,14 +137,12 @@ func handleJSON(w http.ResponseWriter, r *http.Request) {

// Fetch pollen if requested and quota allows
if includePollen {
canPollenProceed, usePollenFree, pollenCost, _ := wallet.CheckQuota(acc.ID, wallet.OpWeatherPollen)
canPollenProceed, _, pollenCost, _ := wallet.CheckQuota(acc.ID, wallet.OpWeatherPollen)
if canPollenProceed {
pollen, pollenErr := FetchPollen(lat, lon)
if pollenErr == nil {
result["pollen"] = pollen
if usePollenFree {
wallet.UseQuota(acc.ID)
} else if pollenCost > 0 {
if pollenCost > 0 {
wallet.DeductCredits(acc.ID, pollenCost, wallet.OpWeatherPollen, nil)
}
}
Expand Down
Loading