# The mandate authority: HTTP contract (v0.15, 2026-09-29) What a buyer, a seller and a verifier can expect from the authority that issues mandates (architecture v0.5 §5.5, §5.9, §6 step 5), so that the authority can be built by anyone and replaced by anyone. The seller's side of this contract (checks 4 to 10, `authority.mjs`) and the buyer's side (`authority.py`, `AuthorityTransport`) already exist; this document is the authority's side, written before the authority, from what the two of them already read. Status (2026-09-11): implemented by agent-authority and live at https://sealedby.dev; the buyer's `HttpAuthorityTransport` (`authority_http.py`) speaks it, and the seller accepted the first cart mandate issued there the same day. Changed in v0.2 (2026-09-19): "Who may ask", the signed envelope on `POST /mandate-requests` and the agents a person linked. Behind a mode that is `off` unless the authority says otherwise on `/health` (`requester`), so a v0.1 buyer keeps working against an authority that has not turned it on. Changed in v0.3 (2026-09-19): "Who approves", a passkey on the approval, with what it signs bound to the one purchase, the evidence kept and its digest in the ledger. Behind a mode of its own, `off` unless the authority says otherwise on `/health` (`approver`); nothing changes for the buyer, the seller or the mandates in any mode. Changed in v0.4 (2026-09-19): "Standing rules", the person's approval given once, ahead of the purchase, within limits their passkey signs; a request that fits is issued without asking. Behind a mode of its own, `off` unless `/health` says `rules: on`. The manual mode stays the default in every sense: with no rule that fits, a purchase waits for the person as it always did. The request, the mandates and the seller do not change. Added the same day with no change of version, since nothing in this API moved: the paragraph "In the buyer's ledger", for a buyer that proposes rules on the person's behalf (`rule.proposed`). Changed in v0.5 (2026-09-19): "Signing in for the person's assistant", the authority as the OAuth 2.1 authorization server of the buyer's entrance for the person's assistant (an MCP server), as the Model Context Protocol's authorization specification asks (revision 2026-07-28). Behind a mode of its own, `off` unless `/health` says `oauth: on`: off, none of its routes exists. Nothing changes in a mandate request, in a mandate or for the seller. Changed in v0.6 (2026-09-20): "Stopping", the person freezing their own agent in one gesture: while stopped nothing is issued for them, by approval or under a rule, and the mandates still out are revoked; resuming takes their passkey. Behind a mode of its own, `off` unless `/health` says `stopping: on`: off, none of its routes exists. A buyer learns one new refusal, `person_paused`; a mandate, the seller and the status document do not change. Changed in v0.7 (2026-09-20): "Signing in to enroll", the person signing in at an identity provider this authority accepts (OpenID Connect) before enrolling a passkey, so that who holds a passkey is no longer the operator's word alone. Asked once, at enrollment and when a passkey has to be replaced; never at a purchase, which is still approved with the passkey. Behind a mode of its own, `off` unless `/health` says `login: on`: off, none of its routes exists and no enrollment writes an entry, as before. With it on every enrollment writes `passkey.enrolled`, which names the issuer that vouched and never the person. Nothing changes for the buyer, the seller or a mandate. Added the same day with no change of version, since nothing in this API moved: a sign-in bound to nobody reads as a request to be let in, which the operator answers in one step ("Whose identity it is"), and the enrollment page says that the ledgers are public. Also added that day, with nothing moving for a buyer, a seller or a verifier: the person's own pages, `/account` ("Stopping", "The person's own pages"), behind the session the stop panel uses. Changed in v0.8 (2026-09-20): "Which assistant asked", a mandate request naming the assistant it came through, with the access token that assistant presented at the buyer's entrance: a token this authority issued, so what it checks is its own signature. The request remembers the grant, the entries of that purchase name it, and the person reads on their own pages which assistant asked for what. Optional in every mode: a request without it is taken as before; one that carries it is refused when it does not hold (`assistant_unknown`). Nothing changes in a mandate or for the seller. Changed in v0.9 (2026-09-20): "The person's own limits", guardrails the person sets for their agent on their own pages, with their passkey, and which the authority holds every purchase to by arithmetic: the most per purchase, a budget and a number of purchases in a rolling period, no second purchase of the same trip too soon, sellers the person blocks, and tighter ones per assistant. Behind a mode of its own, `off` unless `/health` says `limits: on`. They only restrict, within the ceiling the registry holds. A buyer learns one new reason, `limit_exceeded`; a mandate and the seller do not change. Changed in v0.10 (2026-09-20): "Letting a person in, from the web". A sign-in bound to nobody can make the person at once, waiting (`signup: enroll` on `/health`; `request`, the default, is the v0.7 behaviour): they enroll their passkey in that same visit and are nobody at every door until whoever runs the authority lets them in. And that operator, a person of the registry the host's configuration names, does it from their own pages, with their passkey, with an entry that names nobody (`person.admitted`, `person.removed`). Nothing changes for a buyer, a seller or a mandate. Changed in v0.11 (2026-09-21): "Rules the person makes". Until now a standing rule was born from a proposal, by the buyer agent or by the operator, and the person could only confirm it or revoke it. The person now makes a rule themselves, on their own pages, with their passkey, and the rule they make is wider: a budget for one seller (a most per purchase, a total, a number of purchases, up to thirty days), any route. Changing a rule is making another that replaces it, in one signed act. Behind a mode of its own, `off` unless `/health` says `own_rules: on`. `rule.confirmed` gains `form`, `total` and `replaces`; `rule.revoked` gains `replaced_by`. Nothing changes for a buyer, a seller or a mandate: the buyer asks as it always did, and an issuance under such a rule reads as any issuance under a rule. Changed in v0.12 (2026-09-21): "What a seller is trusted with", a number this authority quotes and never invents. A verifier publishes a scorecard it signs; that scorecard carries the trust index a registry gives a seller, one whole number from 0 to 100. The authority keeps it with the moment it was measured and the digest of the document it was read from, shows it on the person's Sellers page beside each seller, and, when the person's own limits ask for a least (`seller_trust_at_least`), refuses a purchase from a seller it is told is under it. A seller with no reading goes through, and the entry of its purchase says it had none. Behind a mode of its own, `off` unless `/health` says `trust: on`. The sellers a person blocks move to the same page. No new entry type and no new reason code: a refusal is `limit_exceeded` with `limit: "seller_trust"`. Nothing changes in a mandate, for a buyer or for a seller. Changed in v0.13 (2026-09-21): "A sign-in to the buyer's own page". The buyer keeps what the person likes (airlines, times, stops) and shows it on a page of its own, where the person signs in here with their passkey. A third scope, `preferences`, asked for by name; a request that names no scope still gets `search purchase`. A grant of `preferences` alone is that sign-in: one access token, no refresh token, and the grant ends as its code is redeemed, so it is never one of the person's assistants and nothing renews it. The consent page says it in those words. The overview links to the buyer's page when the deployment names it (`agent_pages`, for the operator). No new entry type (`access.granted` carries the scope) and no new reason code. Nothing changes in a mandate, for a buyer's purchases or for a seller. Changed in v0.14 (2026-09-26): "The day's allowance". Beside the ceiling per purchase, whoever runs the authority may give a person an allowance per calendar day: what their approved purchases may add up to in one day, in the ceiling's currency. Set when a person is let in (the People page, `allowance` in the admit draft and in `person.admitted`) or from the host (`allowance.set`, a new entry type), and held to at step 6a of the request and again at the moment of issuing: a purchase that would pass it is denied with a new reason, `allowance_exceeded`. The day is the calendar day of the zone the host names (`/health` says `allowance_zone`). The person's own limits (v0.9) stay theirs and only go below it. Nothing changes in a mandate or for a seller; a buyer learns one new reason. Changed in v0.15 (2026-09-29): "A seller known by its UCP profile". A request may name, in a new optional field `seller_profile`, the UCP profile of a seller that publishes no SPIFFE identity and signs its quotes with a key the profile names (`https:///.well-known/ucp`, nothing after it). The quote is then read as that seller signs it (section below), and the same two mandates are issued from what it signed alone; the approval page shows the offer and says that no route or date was signed. No new reason code and no new entry type; a buyer that never names a profile sees nothing new. The authority is a service, not an agent: it runs no model, it decides by fixed policy, and the only judgement in it is the person's, on the approval page. The interfaces are shaped so a bank or a wallet provider can stand in the same place. ## Names | Term | Meaning | |---|---| | issuer | The authority's public base URL, `https://`, no path, no trailing slash. It is the `iss` of every mandate and the base of every URL below | | person, `sub` | An opaque id the authority holds for a human (`mandate.schema.json`): never a phone number, never a name. The buyer learns it from its own allow-list; the authority holds it in its registry with the person's ceiling and the number it notifies | | buyer instance, `act_sub` | The SPIFFE ID of the buyer acting for the person | | buyer key, `cnf_jkt` | RFC 7638 thumbprint of the buyer's stable signing key; the mandate is bound to it | | ceiling | The standing amount, in minor units of one currency, the person allows the buyer to spend per purchase. Set in the registry, never sent by the buyer | | request | One ask for a mandate: one quote, one person, one buyer key. It has a `request_id`, a status, and at most one decision | ## Keys and signatures - `GET /.well-known/jwks.json` publishes the issuing keys: `{keys: [{kty: EC, crv: P-256, x, y, kid, alg: ES256, use: sig}]}`, `kid` equal to the RFC 7638 thumbprint. v0.1 publishes one issuing key and does not rotate it; a rotation would keep the previous key published until its last mandate expired (architecture §3.1), and is not built. The seller caches the set for five minutes and refreshes once on an unknown `kid`. - Every mandate is a compact JWS under `jws-policy.md`: `alg: ES256`, `typ: mandate+jwt`, `kid` mandatory, no `jku`, `x5u`, `jwk`, `x5c`, `crit`. Payload: `mandate.schema.json`. - The authority's ledger entries are signed by the issuing key too, in v0.1 (one key per authority). The signer is `spiffe:///authority/`. ## Endpoints | Method and path | Who calls | What | |---|---|---| | `GET /.well-known/jwks.json` | seller, buyer, verifier | The issuing key set | | `POST /mandate-requests` | buyer | Ask for a mandate: the quote JWS, the person, the buyer instance and key; since v0.2 in a signed envelope ("Who may ask"); since v0.8 it may name the assistant it came through ("Which assistant asked") | | `GET /mandate-requests/{request_id}` | buyer | The request document; `?wait=` (at most 25) holds the answer until a decision or the wait passes | | `GET /approve/{approval_token}` | the person, from the link | The approval page | | `POST /approve/{approval_token}` | the person | `decision=approve`, `deny`, or (after an approval) `revoke`, as the page's form; since v0.3, where the approval takes a passkey, approve is `{decision: approve, credential}` as JSON ("Who approves") | | `POST /rules` | buyer | Propose a standing rule for the person to confirm (since v0.4, "Standing rules"); signed envelope | | `GET /rules/{rule_id}` | buyer | The rule document | | `GET /rules/approve/{approval_token}` | the person, from the link | The rule's page: the limits, Confirm (passkey) and Deny; after a confirmation, what was used and Revoke | | `POST /rules/approve/{approval_token}` | the person | `{decision: approve, credential}` as JSON (the passkey's answer), or `decision=deny` or `revoke` as the page's form | | `GET /enroll/{invitation_token}` | the person, from the operator's invitation | The page where a passkey is enrolled (since v0.3) | | `POST /enroll/{invitation_token}` | the person | The new credential, as JSON; answers JSON | | `GET /mandates/{jti}` | seller (check 10), buyer, verifier, monitor | `authority-status.schema.json`; 404 for a `jti` the authority never issued | | `POST /mandates/{jti}/revoke` | operator | Revokes one mandate; `Authorization: Bearer ` | | `GET /revocations` | verifier | `{as_of, revoked: [{jti, revoked_at, revoked_by}]}`, every revocation the authority holds | | `GET /ledger` | verifier, monitor | The authority's signed chain, same document shape as the buyer's (`ledger-entry.schema.json`) | | `GET /checkpoints` | verifier | The chain's checkpoints beside their witness receipts (`checkpoints-document.schema.json`), since 2026-09-12; see "Checkpoints" | | `GET /.well-known/oauth-authorization-server` | the person's assistant (an MCP client) | Since v0.5, only with `oauth: on`: the authorization server's metadata (RFC 8414); see "Signing in for the person's assistant" | | `GET /oauth/jwks.json` | the buyer's entrance (a resource server) | The key access tokens are signed with; never the issuing key of mandates | | `POST /oauth/register` | an MCP client | Dynamic client registration (RFC 7591), public clients only | | `GET /oauth/authorize` | the person's browser, sent by the client | The authorization request: 303 to its consent page, or back to the client with an error | | `GET`, `POST /oauth/authorize/{id}` | the person | The consent page; `{decision: approve, credential}` as JSON (the passkey's answer) answers `{redirect}`; `decision=deny` as the page's form | | `POST /oauth/token` | an MCP client | `authorization_code` (with PKCE) and `refresh_token`, as a form; the token response, or `{error, error_description}` (RFC 6749, 5.2) | | `GET /stop` | the person | Since v0.6, only with `stopping: on`: opens a session and shows one button that asks the device for a passkey, no credential named; see "Stopping" | | `GET`, `POST /stop/session/{session_id}` | the person | The person's panel once the passkey has said who they are: what is running, Stop, Revoke beside each rule and each assistant's access, Resume. `{decision: signin, credential}` and `{decision: resume, credential}` as JSON; `decision=stop`, `revoke-rule`, `revoke-access` as the page's form | | `GET`, `POST /stop/link/{stop_token}` | whoever holds the person's stop link | Whether the person's agent is stopped, and one button, Stop (`decision=stop`). Nothing else is shown and nothing else can be done here | | `GET /account` | the person | Since 2026-09-20, only with `stopping: on`: opens a session of the same kind as `/stop` does and shows the same one button; see "The person's own pages" | | `GET`, `POST /account/{session_id}[/{section}]` | the person | The person's own pages once the passkey has said who they are: the overview, and `purchases`, `assistants`, `sellers`, `records`; since v0.9, with `limits: on`, also `limits`; since v0.10, for an operator, also `people` (`{decision: admit, person, ceiling}`, `{decision: remove, person}` and `{decision: people-confirm, credential}` as JSON, "Letting a person in, from the web"). `{decision: signin, credential}` as JSON; `decision=revoke-rule`, `revoke-access`, `label-access` as the page's form; on `limits`, `{decision: limits, limits}` and `{decision: limits-confirm, credential}` as JSON ("The person's own limits"); since v0.11, with `own_rules: on`, also `rules` (`{decision: rule, rule}` and `{decision: rule-confirm, credential}` as JSON, "Rules the person makes") | | `GET /login` | the person | Since v0.7, only with `login: on`: what the sign-in is for and one button per identity provider this authority accepts; see "Signing in to enroll" | | `POST /login` | the person | `provider=