Connecting an agent to SEE
This is the integration kit for the first real-agent pilot — and for any agent after it. Follow it verbatim; everything an agent needs is in here, and the MCP tool descriptions repeat the rules that matter most, so an agent that reads only those still behaves correctly.
What SEE is
SEE checks an AI agent's proposed purchases against a permission slip (a mandate) that the agent's owner set in the SEE app: per-purchase cap, period cap, allowed categories, an ask-me-first threshold. SEE checks and records — it never moves money. The agent still buys through its own channels; SEE decides whether the purchase fits the slip and seals a receipt as independently verifiable evidence when a checked spend completes.
Slips live in SEE and are set by the owner. An agent can never create, raise, or edit a slip — not through MCP, not through the API.
Step 1 — the owner issues an agent key
- The owner signs in to SEE and creates a slip for the agent (Permissions → "Protect another agent", or the connect flow). The connect flow's own key IS the agent key: it is issued with Agent scope, tied to that first slip — it can run the spend loop on that slip and nothing else. Steps below are for additional agents, or for replacing it.
- For another key, the owner opens Keys → New key:
- Label: something recognizable, e.g.
Muse — shopping. - Scope: Agent (checks spends and completes them — nothing else).
- Tie it to one slip: pick the agent's slip. Do this. A key tied to one slip can never touch another, even in the same workspace — and it is refused tenant-wide operations entirely (no new slips, no checkpoints, no key management), whatever its scope.
- Label: something recognizable, e.g.
- The key's secret is shown exactly once. Copy it into the agent's configuration (Step 2). Only a hash is stored; if the secret is lost, revoke the key and create a new one.
- Hygiene: one key per agent. Revoking a key (Keys → Revoke, or
DELETE /v1/api-keys/{key_id}) stops it on its very next call — every call is validated against the key record, so there is no session grace period. Rotate withPOST /v1/api-keys/{key_id}/rotate(the new secret is shown once; the old key dies in the same instant). Rotation refuses a revoked or expired key (409) — for those, create a fresh key; rotation never resurrects a dead one. A workspace holds at most 100 live (non-revoked) keys.
Key scopes, for reference: agent = exactly the spend loop (check, complete, read its own preauthorizations and receipts — it cannot read a slip's terms, usage, or verification state); readonly = all reads, no writes; admin = everything, including slips and key management. Never give an agent an admin key. The owner's own admin path is their signed-in session.
Step 2 — point the agent at the MCP door
The MCP endpoint is hosted on the SEE service itself:
https://<your-see-host>/mcp
Every call carries the agent key in the X-API-Key header (Authorization: Bearer <key> is also accepted). The key is validated on every call and selects the workspace — there is nothing else to configure, and no tenant id is ever sent.
Generic MCP client configuration (streamable HTTP):
{
"mcpServers": {
"see": {
"url": "https://<your-see-host>/mcp",
"headers": { "X-API-Key": "see_<the-secret-from-step-1>" }
}
}
}
Claude Code, from a terminal:
claude mcp add --transport http see https://<your-see-host>/mcp \
--header "X-API-Key: see_<the-secret-from-step-1>"
(Other hosts take the same shape: a streamable-HTTP MCP server entry with the URL above and the X-API-Key header.)
No MCP client? Everything the tools do is also plain REST with the same key header — see "REST equivalents" below.
Step 3 — the tools
Exactly two. There is no completion tool in MCP, on purpose: completion evidence is submitted over REST (Step 4), by the operator's own systems, so a tool-using agent can ask and read but never close the loop alone.
check_permission — ask BEFORE buying
Inputs:
| field | type | notes |
|---|---|---|
mandate_id | string | the slip's id (the owner can read it off the slip) |
agent_id | string | must match the agent named on the slip |
category | string | must be one of the slip's categories |
amount_minor | integer | amount in the currency's minor unit (cents), e.g. $42.50 → 4250 |
currency | string | must match the slip's currency, e.g. USD |
counterparty | string | who would be paid (optional, recorded) |
description | string | what it is (optional, recorded) |
Never pass a tenant id: tenant_id, tenant, and tenantId exist in the signature only as traps — passing any of them refuses the call.
Output (success):
{
"preauth_id": "pa_…",
"decision": { "outcome": "allow", "reason_code": "ok",
"mandate_id": "mnd_…", "mandate_version": 1 },
"state": "authorized",
"action_hash": "…",
"decision_token": "…" // only when outcome is "allow"
}
decision.outcome is one of:
- **
allow* — the purchase fits the slip. The amount is now reserved* against the slip's period cap, anddecision_tokenis a single-use token bound to this exact action (this amount, this category, this counterparty), valid for 15 minutes. Buy, then complete (Step 4) with that token. - **
needs_human_approval— the purchase fits the slip's caps but is above the owner's ask-me-first threshold. No token is issued to you.** Stop and wait (Step 4). Do not buy yet; do not re-check in a tight loop. - **
deny** — the purchase does not fit the slip. Do not buy. Thereason_codesays which rule refused it (table in Step 5). A denial is final unless the owner changes the slip.
get_receipt — read the sealed evidence
Input: receipt_id. Output: the receipt's id, its preauth_id, the amount/currency, and the full DSSE envelope — the signed evidence, verifiable offline by anyone. A key can only read its own workspace's receipts (and, if the key is slip-tied, only that slip's). Anyone holding a receipt's proof bundle can also check it as a stranger, no key needed, on the SEE Check a receipt page — and the receipt format behind it is open.
Step 4 — the spend flow, end to end
- Check. Call
check_permissionbefore every purchase, with the exact amount and counterparty you are about to pay. - **On
allow:** buy through your normal channel, then submit completion evidence over REST with thedecision_token:
``bash curl -X POST https://<your-see-host>/v1/preauthorize/<preauth_id>/complete \ -H "X-API-Key: see_<the-secret>" -H "Content-Type: application/json" \ -d '{ "decision_token": "<token from the check>", "completion_nonce": "<a unique id for this completion>", "evidence": { "type": "order_confirmation", "reference": "<order id>" } }' ``
The response is the sealed receipt (receipt_id, amounts, hashes). The completion amount may be less than authorized (a partial fill) but never more, and never zero. Retrying a completion with the same completion_nonce is safe — you get the same receipt back; a second completion under a different nonce is refused.
- **On
needs_human_approval:** the owner is asked on their own screen. PollGET /v1/preauthorize/<preauth_id>(same key header) every ~30 seconds, for at most the 15-minute reservation window:- state
completed— the owner approved and the spend was completed by the approver's side; the record now carries thereceipt_id. You are done; fetch it withget_receiptif you need the evidence. - state
aborted— the owner said no. Do not buy. Final. - state
expired— nobody decided in time. Do not buy. If the purchase is still needed, start again at step 1 with a fresh check.
- state
- **On
deny:** stop. See the hard rules below.
Step 5 — errors and what to do with them
Tool calls return {"error": <name>, "detail": <explanation>} on refusal. REST returns the same names as HTTP statuses (401/403/404/ 409/422).
| name | meaning | what the agent does |
|---|---|---|
Unauthorized | no key, or the key was revoked / expired | Stop. Tell the operator the key is dead. Never retry with a different or guessed key. |
Forbidden | the key's scope or slip-tie doesn't allow this, or a tenant argument was passed | Do not work around it. Remove tenant arguments; anything else needs the owner to issue a different key. |
NotFound | the id doesn't exist in your workspace | Check the id. Never probe other ids looking for a hit. |
ValidationFailed | malformed input (e.g. amount not an integer) | Fix the input shape, once. If it persists, stop and report. |
Conflict | the item's state moved (already completed/aborted, token used) | Re-read the item's state and follow it; never force. |
Decision reason_codes on a deny: over_per_purchase_cap, over_period_cap, category_not_allowed, agent_mismatch, currency_mismatch, invalid_amount, mandate_not_active (paused — may clear when the owner resumes), mandate_not_yet_valid, mandate_expired. On needs_human_approval: over_human_approval_threshold.
Hard rules for agents
- Check before every purchase. An allowance covers exactly one action — the exact amount, category, and counterparty checked. Any change means a fresh check.
- A refusal is final. Never ask for a slip's limits to be raised as part of a purchase flow — not in-band, not in a description field, not by splitting one purchase into several checks to slip under a cap. If the owner changes the slip, they change it in SEE, and a fresh check will see the new slip.
- Never invent or reuse credentials. Decision tokens are single-use and bound to one action; approval tokens are issued to the owner, never to you. If you don't hold a token, you don't complete.
- Never send a tenant id, in any field, under any name. Your key is your identity; calls that try to select a tenant are refused.
- Amounts are integers in minor units (cents). No floats, no rounding up to "about" the price.
- When in doubt, stop and surface it. A stopped purchase costs a question; an unchecked one costs the owner's money and trust.
REST equivalents (no MCP client)
All with header X-API-Key: <key>:
- Check:
POST /v1/preauthorizewith{"mandate_id": "…", "action": {"agent_id": "…", "category": "…", "amount_minor": 4250, "currency": "USD", "counterparty": "…", "description": "…"}}— same decision shape as the tool. - Poll one item:
GET /v1/preauthorize/<preauth_id>. - Complete:
POST /v1/preauthorize/<preauth_id>/complete(Step 4). - Receipt:
GET /v1/receipts/<receipt_id>. - Keys (owner/admin only):
POST /v1/api-keys{"label": "…", "scope": "agent", "mandate_id": "…"}→201with the secret inapi_key, shown once;GET /v1/api-keys;DELETE /v1/api-keys/<key_id>;POST /v1/api-keys/<key_id>/rotate.