docs · the agent door
Let agents act on your platform.
On your terms, exactly once.
New to Bubblio? Start with the widget: Next.js guide → · script-tag guide →
The widget is the door for humans. The agent door is the other one — a stateless MCP endpoint (https://api.bubblio.dev/agent/{doorId}/mcp) into the same brain, knowledge, and receipts. Visiting agents ask questions for free; with the pieces on this page they can also act: run functions you declare in code, gated per action by you, through a quote→confirm machine that executes exactly once.
How it works
- 1Your code declares actions.defineDoorActions binds each action’s schema and its handler in one object — the shape lives next to the implementation, never in a dashboard form. On deploy, sync pushes the shapes to Bubblio. Shapes only: a sync can never open anything.
- 2You decide, per action, in the dashboard.Synced actions arrive hidden. The dashboard shows your code’s policySuggestion next to each one and waits for your decision: keep it hidden, open it to agents carrying a key, or open it to any agent. A deploy can never open an action; closing one never needs a deploy.
- 3Agents quote, confirm, and your handler runs.A visiting agent pins the exact call with quote, then executes it with confirm — exactly once, receipted, HMAC-signed to your callback route. For keyed actions the user’s identity arrives as the same signed ?ctx your widget tools already use, so your existing route resolves `user` with zero new code.
Step 1 — Declare actions next to their handlers
One file, e.g. lib/bubblio/door.ts. Each action is a schema and a handler in one object; policySuggestion is a hint the dashboard displays — it is never applied. Handlers get (args, user, meta), the same signature as every widget tool.
Step 2 — Merge the handlers, sync on cold start
The door reuses the callback route you (may) already have for widget tools — spread door.handlers in, and fire door.sync at module scope. Sync is idempotent: unchanged shapes write nothing, actions you stop declaring are flagged “no longer in code” (never deleted), and new actions always land hidden, awaiting your decision in the dashboard.
After a deploy that ships new actions, the SDK prints one line with how many are awaiting your decision, and Bubblio emails you a summary — so a new capability never slips by unnoticed. Destructive-looking names sync anyway (a deploy should never wedge on a heuristic) with the suggestion dropped and a warning logged.
Step 3 — Decide in the dashboard
Open the door on the Agent door page (one toggle — questions only until you say otherwise). Every synced action then waits for a decision:
Accepting the code's suggestion is one click; declining (“keep hidden”) sticks — a declined action stays declined across every future deploy. Nothing runs until you decide, and closing an action takes effect immediately, no deploy involved.
Step 4 — Let users connect their agents (managed keys)
Keyed actions need identity. A user clicks “Connect your AI agent” on your site, gets a bak_… key once, and pastes it into their agent. The whole backend is one route — you supply only your session lookup:
GET lists the signed-in user's keys, POST mints one (the raw key appears once, there — show it, don't store it), DELETE revokes, scoped to the user's own keys. Everything getUser returns except hint is signed into the identity and becomes your handlers' user argument on every keyed confirm.
Test your door
Your door speaks standard MCP (Streamable HTTP, JSON responses) — any client works. The exact URLs, plus copy buttons, live on your Agent door page; every test visit shows up there as a real conversation.
Who holds what — the custody and authority doctrine
- Code declares. The dashboard governs.Sync writes shapes; only the owner, logged into the dashboard, sets policy. A deploy can never open a capability, and closing one never needs a deploy. This is why sync may run with just an API key: a shape-only write can’t open anything.
- Exactly once, or a replayed receipt.quote pins the exact tool and args and expires in 10 minutes; confirm consumes the quote with a winner-only update. A duplicate confirm returns the original receipt — never a second execution. Every confirm is receipted and rate-capped.
- Bubblio holds the key, your server holds the identity.Managed bak_ keys are stored hashed; the raw key never travels to your servers (callbacks carry only { tier: ‘keyed’, keyId }). Identity is a JWT only your server could have signed — Bubblio stores it encrypted and can neither read it into being nor forge it, since it keeps only a hash of your API key. On confirm it arrives as the same signed ?ctx the widget uses.
- Rotating your API key rotates the identity secret.Identity tokens are signed with a secret derived from your Bubblio API key (~90-day lifetime). After a rotation, keyed confirms resolve a null user until each user re-mints through your route — plan rotations, and tell agent-connected users.
- Payments are a later rung.Actions run against your own backend. Bubblio never holds funds.