Sealgraf Open the app

Connecting an agent to Sealgraf

This is the integration kit for connecting an agent to Sealgraf. 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 Sealgraf is

Sealgraf checks an AI agent's proposed purchases against a permission slip (a mandate) that the agent's owner set in the Sealgraf app: per-purchase cap, period cap, allowed categories, an ask-me-first threshold. Sealgraf checks and records — it never moves money. The agent still buys through its own channels; Sealgraf decides whether the purchase fits the slip and seals a receipt as independently verifiable evidence when a checked spend completes.

Slips live in Sealgraf 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

  1. The owner signs in to Sealgraf and creates a slip for the agent (Permissions → "Set limits for 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.
  2. For another key, the owner opens Keys → New key:
    • Label: something recognizable, e.g. Shopping runs.
    • 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.
  3. The key's secret is shown exactly once. Copy it into the agent's configuration (Step 2). If the secret is lost, revoke the key and create a new one.
  4. Hygiene: one key per agent. Revoking a key (Keys → Revoke, or DELETE /v1/api-keys/{key_id}) takes effect immediately — the key's very next call is refused. Rotate with POST /v1/api-keys/{key_id}/rotate (the new secret is shown once; the old secret stops working at once). Rotation refuses a revoked or expired key (409) — for those, create a fresh key; rotation never resurrects a dead one.

If an approval is still pending when you rotate, finish or let it lapse first: a rotated successor key cannot claim the old key's approval tokens (claims belong to the key that ran the check), so an in-flight approval is completed by the owner, aborted, or re-checked under the new key.

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 Sealgraf service itself:

https://<your-sealgraf-host>/mcp

Every call carries the agent key in the X-API-Key header (Authorization: Bearer <key> is also accepted). The key 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-sealgraf-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-sealgraf-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: finishing a spend — claiming an approval's decision token and submitting completion evidence — is a REST act (Step 4), so a tool-using agent can ask and read over MCP, and the spend itself closes only through an explicit REST call carrying the token and the evidence.

check_permission — ask BEFORE buying

Inputs:

fieldtypenotes
mandate_idstringthe slip's id (the owner can read it off the slip)
agent_idstringmust match the agent named on the slip
categorystringmust be one of the slip's categories
amount_minorintegeramount in the currency's minor unit (cents), e.g. $42.50 → 4250
currencystringmust match the slip's currency, e.g. USD
counterpartystringwho would be paid (optional, recorded)
descriptionstringwhat it is (optional, recorded)
idempotency_keystringoptional, 8–200 chars. Repeating a check with the same key and the same action returns the original check (marked "replayed": true) instead of reserving twice; the same key with a different action is refused (Conflict)

Arguments are validated strictly: a value of the wrong JSON type (a string amount, a float, a boolean) is refused, never coerced, and any field outside this table is refused by name — nothing is silently dropped.

Never pass a tenant id. Tenant context is derived from the authenticated key, never from call arguments: passing tenant_id, tenant, or tenantId refuses the call.

Output (success):

{
  "preauth_id": "pa_…",
  "decision": { "outcome": "allow", "reason_code": "ok",
                "mandate_id": "mnd_…", "mandate_version": 1 },
  "state": "authorized",
  "action": { "agent_id": "…", "category": "…", "amount_minor": 4250,
              "currency": "USD", "counterparty": "…", "description": "…" },
  "action_hash": "…",
  "token_expires_at": "…",     // when the decision token dies (null on a deny)
  "ask_expires_at": "…",       // pending asks only: when the owner's decision window closes
  "decision_token": "…"        // only when outcome is "allow"
}

decision.outcome is one of:

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 Sealgraf Check a receipt page — and the receipt format behind it is open.

Step 4 — the spend flow, end to end

  1. Check. Call check_permission before every purchase, with the exact amount and counterparty you are about to pay.
  2. **On allow:** you hold a decision_token. Buy through your normal channel, then submit completion evidence over REST:

``bash curl -X POST https://<your-sealgraf-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", "counterparty_receipt_ref": "<order id>", "note": "<what was bought>" } }' ``

Evidence seals exactly these fields: completed_amount_minor (optional; defaults to the authorized amount), counterparty_receipt_ref, note, and completed_at. The annotations type and source are also accepted but are not sealed. Any other evidence key is refused (ValidationFailed, naming the key) and nothing is sealed — a refused completion can be corrected and retried with the same nonce.

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.

  1. **On needs_human_approval:** no token is issued at check time. The owner is asked on their own screen. Poll GET /v1/preauthorize/<preauth_id> (same key header) every ~30 seconds. Judge the record by its state and the deadline for its phase — ask_expires_at while it is pending, token_expires_at once it is authorized (see "The two windows" below). A state can outlive its window, and an expired window always wins. States you can see:
    • pending_approval — still waiting on the owner. Keep polling, at most until ask_expires_at. Do not buy; do not re-check in a tight loop.
    • authorized — the owner approved. The spend is yours to finish. A decision token was issued for you at approval time; claim it, then buy, then complete: ``bash curl -X POST https://<your-sealgraf-host>/v1/preauthorize/<preauth_id>/decision-token \ -H "X-API-Key: see_<the-secret>" # → { "decision_token": "…", "token_expires_at": "…" } ` Claim **before** you buy: the claim proves the channel works and starts from a live window (it closes at the token_expires_at` on the record). Only the key that ran the original check can claim — a different key, even in the same workspace, is refused. Then buy, and complete exactly as in step 2, with the claimed token and your purchase evidence.
    • completed — a receipt exists (receipt_id is on the record). Whoever completed it, the spend is closed. If you have not bought yet, do not buy — the owner may have completed it from their own screen. Fetch the receipt with get_receipt if you need the evidence, and report the outcome.
    • aborted — the owner said no. Do not buy. Final.
    • expired — the window closed before a decision, or before you completed. Do not buy. If the purchase is still needed, start again at step 1 with a fresh check.

A completion that answers already completed means the same thing as the completed state above: re-read the record and follow it; never force, never buy twice.

  1. **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).

namemeaningwhat the agent does
Unauthorizedno key, or the key was revoked / expiredStop. Tell the operator the key is dead. Never retry with a different or guessed key.
Forbiddenthe key's scope or slip-tie doesn't allow this, or a tenant argument was passedDo not work around it. Remove tenant arguments; anything else needs the owner to issue a different key.
NotFoundthe id doesn't exist in your workspaceCheck the id. Never probe other ids looking for a hit.
ValidationFailedmalformed input (e.g. amount not an integer, an unknown field, an unsupported evidence key)Fix the input shape, once. If it persists, stop and report.
UnknownToolthe MCP tool name doesn't exist hereCall check_permission or get_receipt; nothing else is offered.
Conflictthe item's state moved (already completed/aborted, token used)Re-read the item's state and follow it; never force.
not_approved_yet (409, claim)the owner hasn't decidedKeep polling the record (Step 4.3).
decision_token_expired (409, claim)a window closed: the ask's (ask_expires_at passed undecided) or the approval token's (token_expires_at passed)Do not buy. Fresh check if the purchase is still needed.
already_completed (409, claim/complete)the spend is already closedRe-read the record; follow the completed state. Never buy twice.
claim answered 404this preauthorization isn't yours to claim (not created by your key)Stop. You can only claim tokens for checks your own key ran. Tell the operator if you expected otherwise.

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.

The two windows

A check record carries two distinct deadlines. Never judge one phase by the other's clock:

fieldgovernslength
ask_expires_atthe pending phase — how long the owner has to decide an ask24 hours from the check
token_expires_atthe authorized phase — how long a minted decision token lives900 seconds from the allow / approval that minted it

While an ask is pending_approval, ask_expires_at is the deadline that matters: the ask is decidable for the whole 24 hours, and its amount stays reserved against the slip's period cap until then. Once the owner approves, a token is minted and token_expires_at (approval time + 900s) is the only deadline — claim and complete inside it. An ask the owner never decides expires at ask_expires_at; it never turns into an approval on its own.

Ask shaping (the faucet)

Asks spend the owner's attention, so Sealgraf shapes how fast a slip can generate them. Three more deny reason codes exist for this — they are decisions, not errors: the check answers with the normal success response (HTTP 200 family) carrying outcome: "deny" and a sealed record, exactly like a scope denial — never an error status — and the response carries the governing numbers in a shaping_detail object. None of them mints a token, reserves an amount, or notifies the owner.

reason_coderulegoverning numbers (in shaping_detail)what the agent does
ask_budget_exhaustedat most 3 asks waiting undecided per slip, and at most 10 new asks admitted per rolling hour per slippending_count + pending_cap, or admitted_last_hour + hourly_ask_limit + window_resume_atStop asking. Wait until a waiting ask is decided or the stated window passes.
ask_cooling_offa storm — 5 admitted asks in 10 minutes, or 3 owner refusals in an hour — puts the slip into cooling-off for 1 hourcooling_off_untilStop asking until that instant. The hour never extends; attempts during it change nothing.
action_previously_deniedthe owner already refused this exact action (same amount, merchant, category)denied_preauth_id + denied_amount_minorDo not re-ask this action unchanged — the answer is recorded. A materially changed action (different amount, merchant, or category) is a new check.

Two related behaviors, neither of them an error either:

Hard rules for agents

  1. Check before every purchase. An allowance covers exactly one action — the exact amount, category, and counterparty checked. Any change means a fresh check.
  2. 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 Sealgraf, and a fresh check will see the new slip.
  3. Never invent or reuse credentials. Decision tokens are single-use and bound to one action. A token reaches you exactly two ways: in the check response on allow, or by claiming it after the owner approves (Step 4.3) — with the same key that ran the check. Approving is never yours to do, and no token lets you spend beyond the exact action that was checked. If you don't hold a token, you don't complete.
  4. 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.
  5. Amounts are integers in minor units (cents). No floats, no rounding up to "about" the price.
  6. 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>: