Upgrade Through MCP
One tool, five actions. An AI client takes a Free user from subscription discovery to paid tier without ever leaving the chat. The only browser step is clicking the Stripe Checkout URL.
An enterprise-grade memory system has to be reachable end-to-end from the AI itself. If a Free-tier operator hits a paid feature and the AI's only response is "go to the website and upgrade," the system has failed the operator. Gnosis exposes the entire subscription journey as one MCP tool, account, with an action argument that selects what to do. The AI calls account(info), presents options via account(plans), mints a Stripe Checkout URL via account(checkout, sku_id), and hands it to the user. Stripe handles the payment surface (tax, card collection, fraud protection). The webhook flips the user's tier server-side. The next init_core_memories call reflects the new state.
URLs Only, Never Silent Charges
The account tool returns Stripe URLs and never performs a transaction. The browser click is the single irreducible step for compliance reasons: financial actions require user consent through the payment surface itself. Card numbers never flow through the MCP channel. The AI's job is to discover, recommend, mint URLs, and confirm post-purchase state. The user's job is one click.
Five Actions, Two Round Trips
The full journey from "I want to share memories with a teammate" to "the shared collection works" is two round trips with the AI client. The first round trip runs account(info) and account(plans) to establish context. The second runs account(checkout, sku_id) to mint the URL. Everything else happens in the user's browser and on Stripe's webhook.
account(action="info")
Returns the caller's current tier, subscription status, supporter badge, capabilities, and which SKUs they can move to. Always visible. Free callers see tier: "free" plus the upgrade catalog.
tier
One of free, plus, pro, og, truebeliever, ogdev, business, enterprise. The authoritative state. Surfaces in init_core_memories too so the AI sees it on session bootstrap.
subscription_status
none, active, canceling, past_due, canceled, or permanent. canceling means the user requested cancellation; the tier stays active until the period ends.
capabilities
Cumulative capability rollup: memory_pool (cap), agents (count or -1 for unlimited), collections_write (boolean), collections_admin (boolean), signals (boolean).
can_upgrade_to / can_downgrade_to
SKU ids for the transitions actually available. Free shows all paid SKUs in can_upgrade_to; Plus shows Pro SKUs upgrade and Free downgrade; Pro shows Plus and Free downgrade. Permanent tiers (og, truebeliever) show both arrays empty.
Other fields: supporter_badge and supporter_total_cents for donation-earned badges, is_permanent for the no-recurring-billing tiers, and endpoint paths (plans_endpoint, checkout_endpoint, portal_endpoint) so the AI never has to guess where the catalog lives.
Surfacing tier at init time
The same tier and subscription_status fields appear on the init_core_memories response so the AI knows the operator's plan on the very first turn. This eliminates a follow-up account(info) call when the AI just needs to know whether a feature is reachable. For deeper context (capabilities, available transitions, supporter badge), account(info) is the dedicated lookup.
account(action="plans")
Public SKU catalog. Returns every buyable product with display name, description, live Stripe-fetched price, cadence, and what each unlocks. No authentication required; works from anonymous contexts (stdio without an AGENT_ID).
{
"skus": [
{
"sku_id": "plus_monthly",
"kind": "subscription",
"tier": "plus",
"cadence": "monthly",
"display_name": "Plus (monthly)",
"description": "5,000 memories, read and write to shared collections.",
"unlocks": ["shared_collection_write", "memory_pool:5000"],
"unit_amount_cents": 700,
"currency": "usd",
"interval": "month",
"configured": true
},
{
"sku_id": "pro_monthly", "tier": "pro", "cadence": "monthly",
"display_name": "Pro (monthly)",
"unlocks": ["create_collections", "admin_collections", "unlimited_agents", "memory_pool:-1"],
"unit_amount_cents": 2500,
...
}
],
"early_access_open": false,
"pricing_url": "https://gnosismemory.com/pricing",
"checkout_endpoint": "/api/checkout"
}
Subscription SKUs always appear. One-time supporter SKUs (Benefactor, Founder) only appear when early access is open (server flag EARLY_ACCESS_OPEN). The donate flow always works; the user picks the amount when the AI calls account(donate, amount_cents).
Live prices, static metadata
unit_amount_cents and currency come from Stripe at request time (the worker fetches each price_ ID and returns the current amount). Display name, description, and capability unlocks are static metadata maintained alongside the SKU registry so marketing copy never drifts from what the AI presents to the user. Changing a Stripe price in the dashboard is the only edit needed to roll out a price change.
account(action="checkout", sku_id=…)
Mints a Stripe Checkout Session for the given SKU and returns the URL. The AI hands the URL to the user. The user clicks, completes payment in their browser, and Stripe redirects them back. The webhook fires, the tier flips server-side, and the next init_core_memories call reflects the change.
Present the Stripe Checkout URL verbatim — it is https://checkout.stripe.com/... — do not paraphrase the domain. The user needs to see the canonical Stripe hostname so they can confirm they're entering card data on Stripe's hosted page.
account(action="checkout", sku_id="plus_monthly")
// returns:
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
"sku_id": "plus_monthly",
"note": "Hand this URL to the user. They click it and complete payment..."
}
The worker handles tax (Stripe Tax enabled, EU OSS / UK VAT inclusive pricing), tax ID collection for B2B buyers, idempotency (a five-minute bucket prevents accidental double-creates), and metadata tagging so the webhook can route the resulting subscription to the right account. None of that surfaces in the MCP call; the AI sees only the URL.
Why the LLM can't complete the purchase itself
Stripe's Checkout is a hosted page that collects card data, runs fraud checks, completes Strong Customer Authentication where required, and returns a signed completion event via webhook. Embedding card collection in the MCP channel would require taking on PCI compliance, fraud surface, and SCA flow handling that the AI cannot meaningfully complete on the user's behalf. The single-click handoff is both safer (the user sees what they're paying for on Stripe's UI before the charge) and simpler (no payment plumbing in the AI client).
account(action="portal")
Mints a Stripe Customer Portal URL where the user changes plan, updates their card, views invoices, or cancels. Same return shape as checkout: just a URL the AI hands to the user.
For Free users with no Stripe customer record, the action returns a structured payload pointing them to account(plans) + account(checkout) instead of an error. The AI treats the absence of a portal URL as "user has no active subscription; show them how to start one."
// For an active subscriber:
account(action="portal")
// returns: {portal_url: "https://billing.stripe.com/p/session/...", note: "..."}
// For a Free user with no purchase history:
account(action="portal")
// returns: {portal_url: null,
// message: "No active subscription. Call account(plans) + account(checkout) to upgrade.",
// plans_endpoint: "/api/plans"}
For permanent-tier users (Benefactor, Founder, internal developer accounts), the portal works for invoice access but cannot downgrade the account; those tiers are immune to webhook tier changes by design.
account(action="donate", amount_cents=…)
Mints a Checkout URL for a one-off variable-amount donation. The user picks the amount (in currency minor units; e.g., cents for USD). Donations accumulate against supporter_total_cents and unlock supporter badges by cumulative threshold: 500c → supporter, 5000c → supporter_silver, 25000c → supporter_gold. The webhook awards the highest threshold reached.
account(action="donate", amount_cents=2500)
// returns:
{
"checkout_url": "https://checkout.stripe.com/c/pay/...",
"amount_cents": 2500,
"note": "Hand this URL to the user. One-time donation..."
}
Amount bounds are 100 to 500000 cents. The donation Checkout session uses the same tax and ID collection as the subscription Checkout, so EU/UK donors see VAT inclusive pricing.
The Full Flow, End to End
A Free-tier operator asks their AI to share a finding with a teammate. The AI attempts collection_manage(action="create"), receives a tier_required response from the server, and walks the user through the upgrade using the account tool.
Scenario: free user upgrades to Plus in two round trips
Step 1: The AI hits a tier wall.
Step 2: The AI confirms tier and presents options.
The AI presents the two relevant SKUs in plain language: "To create shared collections you'll need Pro at $25/month. To write to existing shared collections, Plus at $7/month is enough."
Step 3: The user picks. The AI mints the URL.
The AI hands the URL to the user: "Click this to upgrade to Pro monthly: https://checkout.stripe.com/c/pay/cs_live_...". The user clicks, enters their card on Stripe's hosted page, completes payment, and is redirected back.
Step 4: The webhook fires. Tier flips.
Stripe sends checkout.session.completed to the worker. The worker reads metadata.sku_id from the session, resolves the matching tier grant from the SKU registry, and PATCHes the backend's /account/by-customer/…/tier endpoint. The backend updates gnosis_accounts.tier and invalidates its settings cache synchronously. Wrapper's 60-second tier cache expires within the minute (or is busted on the next init_core_memories, whichever comes first).
Step 5: The originally-blocked operation now works.
The entire journey runs through the AI. The user clicked once, on a Stripe-hosted page, and never left their chat client.
Design Notes
Why account is always visible
Paid tools (collection_manage, agent_manage) are stubbed for free callers so the LLM can discover what's reachable on a higher tier without being able to call them. The account tool is the inverse: it's how that discovery becomes actionable. Hiding it from Free callers would defeat the purpose. The same tool is returned to every tier.
Why one tool with actions, not five separate tools
The five operations (info, plans, checkout, portal, donate) are tightly coupled — they share an audience (the upgrade-recommender LLM), a domain (Stripe-backed billing), and a usage envelope (returns URLs, never charges). Listing five tools instead of one bloats the tool advertisement that every LLM reads on every call and makes the surface harder to learn. One tool with an action argument matches the signal and collection_manage patterns the rest of the surface already uses, and keeps the LLM's tool-selection step constant size as new account operations are added.
Authentication: three accepted paths
The worker endpoints these actions call (/api/account/subscription, /api/plans, /api/checkout, /api/billing-portal, /api/donate) accept three authentication paths:
- Browser session cookie (existing path, untouched).
- OAuth Bearer access token (for MCP clients holding the user's token directly).
- Internal worker-secret + X-User-ID (used by the pod's MCP wrapper when it proxies tool calls on behalf of the calling user).
Same identity payload shape on the worker side regardless of which path was used. The wrapper uses the third path because it doesn't hold the user's access token at the python-service hop; it holds the shared worker secret that already gates every internal proxy route.
Tier capability matrix
The capabilities returned by account(info) mirror the tier matrix maintained server-side. Free: 500 memories, 0 agents, no shared-collection writes. Plus: 5,000 memories, read and write to shared collections (no admin/create). Pro: unlimited memories (subject to service quality rate limits), unlimited agents, collection creation and administration. Permanent tiers (Benefactor / Founder / internal developer accounts) sit at or above Pro and are immune to webhook tier changes. The matrix is the single source of truth; the MCP action surfaces it without re-deriving.
Legacy tool names
The earlier surface exposed five separate tools: subscription_info, available_plans, start_checkout, manage_subscription, and start_donation. Those names are still recognized by the MCP wrapper and transparently rewritten to the matching account action, so existing clients keep working without code changes. New integrations should call account directly.