Signed mandates
A signed mandate is an ed25519-signed authorization request from an agent to act on a merchant site. It is the central object of Sill’s Transactional mode: every governed action — a charge, a refund, a privileged read — passes through one. A mandate carries the agent’s identity, the intent (skill plus parameters), a replay-defense window, and an envelope holding the signature and the key identifier that produced it. The mandate’s signed body is canonicalized with RFC 8785 JCS and prefixed with a versioned domain-separation tag before signing, so the same ed25519 key can never be tricked into signing an agent card or audit record by collision.
Mandate IDs are prefixed ULIDs of the form mnd_<26 Crockford-base32 chars>.
A mandate is a two-part object: a signed body (everything covered by the signature) and an envelope (signature metadata, not itself signed).
{ "signed": { "mandate_id": "mnd_01J9XYZABCDEFGHJKMNPQRSTVW", "site_id": "01J9XYZACCOUNTSITEEXAMPLEAB", "agent_id": "agent_anthropic_claude", "issued_at": "2026-06-22T14:03:11.000Z", "expires_at": "2026-06-22T14:03:41.000Z", "nonce": "F3RkP2x9Tn5sQwAaC1bD7g", "replay_window_seconds": 30, "principal": { "type": "human", "verification": "delegated" }, "intent": { "action": "place_order", "merchant": "example-merchant", "sku": "ACME-WIDGET-42", "max_amount": 4999, "currency": "USD" }, "protocol_context": { "protocol": "a2a", "version": "1.2" } }, "envelope": { "algorithm": "ed25519", "key_id": "agent_anthropic_claude#k1", "signature": "MEQCIB...86-char-unpadded-base64url-ed25519-signature..." }}Values above are illustrative shapes drawn from the verifier’s expected formats; real mandate IDs, nonces, and signatures are random.
Signed body
Section titled “Signed body”Every field inside signed enters the signing input via canonicalize(signed). An intermediary cannot mutate any of these fields without invalidating the signature.
| Field | Type | Purpose |
|---|---|---|
mandate_id | mnd_<ULID> | Unique mandate identity. Replay-burned after first verification. |
site_id | ULID | The Sill site this mandate addresses. |
agent_id | agent_<slug> | The signing agent, resolved against the identity registry. |
issued_at | ISO-8601 UTC | When the agent minted the mandate. 60s skew tolerance. |
expires_at | ISO-8601 UTC | Hard expiry. Must satisfy expires_at > issued_at + 1s. |
nonce | 22-char base64url | 16 random bytes. Replay defense. |
replay_window_seconds | integer [1, 600] | How long verifiers must remember the nonce. Must cover the validity window. |
principal | object | Party the agent acts for. Type: `human |
intent | object | The requested action, merchant, amount ceiling, currency, and skill-specific parameters. |
protocol_context | object | The boundary protocol (a2a, ap2, mcp) and version. |
The mandate envelope is capped at 8 KB at the HTTP boundary before parsing — oversize requests reject without consuming verifier CPU.
Envelope
Section titled “Envelope”| Field | Type | Purpose |
|---|---|---|
algorithm | "ed25519" | Locked single value. |
key_id | string | The agent’s registered key that produced the signature. Looked up as (agent_id, key_id). |
signature | 86-char base64url | The ed25519 signature over the domain-separated signing input. |
Intent shapes
Section titled “Intent shapes”intent.action selects the intent shape. The verifier enforces a CLOSED key set per action — unknown fields, or fields belonging to a different action, reject at the signing-input boundary.
- Order intents (
place_orderand similar) carry a SKU or a boundedline_itemsarray (one-of),max_amountandcurrency, and optional typed sub-objects:shippingconstraint,discount.code,attributesbounded metadata map, and aquantityinteger in[1, 100]. - Refund intents (
request_refund) carryoriginal_mandate_id, a closedreason_codeenum, and ascopeof"full"or{ line_items: [...] }— and nothing else. The refund amount is never agent-assertable; the rail derives it server-side and the signedmax_amountonly caps it.
Signing input — domain-separated, canonicalized
Section titled “Signing input — domain-separated, canonicalized”The bytes the agent signs (and the verifier verifies against) are:
utf8("sill-mandate-v1") || 0x00 || utf8(JCS(signed))The versioned tag "sill-mandate-v1" closes the cross-protocol signature-confusion surface: Sill’s ed25519 keys must not accept any byte sequence that could collide with another signed surface (agent card, ARD trust manifest, audit envelope). The null byte is an unambiguous prefix/body boundary — JCS-canonical JSON never emits a literal \x00 (a U+0000 inside a string is escaped; the object root starts with {), so a canonicalized body cannot collide with the tag prefix.
If the construction ever changes, deployed mandates continue to verify under -v1 while new mandates use a new tag. This is the same versioning posture the agent card and ARD catalog take.
Lifecycle
Section titled “Lifecycle”A mandate flows through a fixed sequence of stages at Sill’s edge. Each transition is observable in the audit log.
flowchart TD
A[Agent mints mandate] --> B[POST to Sill edge]
B --> C{Shape, size, liveness, protocol allowlist}
C -- fail --> R1[Reject]
C -- pass --> F[Resolve agent in registry]
F -- unknown / revoked --> R1
F -- ok --> G[ed25519 signature verify]
G -- invalid --> R1
G -- valid --> H{Delegation chain?}
H -- present --> I[Verify chain links]
I -- invalid --> R1
I -- ok --> J[Nonce check-and-record]
H -- absent --> J
J -- replay --> R1
J -- fresh --> K[Policy evaluation]
K -- reject --> D1[Decision: rejected]
K -- escalate --> L[Pending human review]
K -- approve --> D2[Decision: approved]
L -- approve --> D3[escalated_approved]
L -- reject --> D4[escalated_rejected]
D2 --> P[Authorize via processor]
D3 --> P
P --> Q[Signed, Merkle-chained audit record]
The verifier runs the pipeline in deliberate cost order: cheap synchronous checks first (format, expiry, replay window, protocol allowlist), then registry resolution, then the expensive ed25519 verification, then delegation-chain checks if a chain is present, then the nonce check-and-record. The ordering means malformed or DoS-shaped traffic never reaches the signature path, and a forged mandate never burns chain-verify CPU or its nonce.
Audit decisions
Section titled “Audit decisions”Every mandate ends in exactly one audit decision:
approved— policy passed, processor was asked to authorize.rejected— policy rejected at the edge.escalated_approved— went to a human reviewer; reviewer approved.escalated_rejected— went to a human reviewer; reviewer rejected.verification_rejected— failed pre-policy verification (signature, replay, agent unknown, etc.).rejected_post_verify— verified but rejected by a later rail-side gate.
See the policy engine for how rules drive approve | escalate | reject, and human-in-the-loop for the escalation surface.
Replay defense
Section titled “Replay defense”Every mandate carries a random 16-byte nonce and a replay_window_seconds integer between 1 and 600. The verifier records the nonce on first sight and rejects on second sight within the window. The window must cover the validity window: replay_window_seconds * 1000 >= expires_at_ms - issued_at_ms. A window too short to cover validity is a shape error.
Replay defense uses a two-layer composition at the edge: an in-isolate Map for the hot path and a KV store for cross-isolate persistence. The verifier sees only a three-valued result (fresh | replay | unavailable); when neither layer can answer, the mandate is rejected as verification_unavailable (HTTP 503), not silently approved. Fail-closed is the load-bearing invariant.
Delegation chains (optional)
Section titled “Delegation chains (optional)”A mandate MAY include a delegation_chain — an ordered list of agent-to-agent authority handoffs, each independently signed by its delegator under a distinct "sill-delegation-v1" domain tag. The verifier checks contiguity, validity windows, monotone scope narrowing (authority can only shrink down the chain, never grow), and every link signature. Chains are bounded (max 4 links, max 16 scope entries per link). An absent chain is valid — single-agent flows are unaffected.
Chain verification runs AFTER the mandate’s own signature verifies and BEFORE the nonce is burned: a chain-invalid mandate must not consume its nonce, and a forged mandate must not buy chain-verify CPU.
Rejection taxonomy
Section titled “Rejection taxonomy”The verifier produces exactly one internal rejection reason per failure. Several identity-class reasons are deliberately coalesced into a single identity_check_failed value on the agent-facing response (so the wire surface never tells a probing caller which identity check failed); the granular internal reason is preserved in the audit record.
| Internal reason | HTTP | Agent-facing reason | Meaning |
|---|---|---|---|
malformed | 400 | malformed | Shape, format, or canonicalization failure. |
oversize | 413 | oversize | Body exceeded the mandate body cap at the HTTP boundary. |
agent_id_malformed | 400 | agent_id_malformed | agent_id did not match agent_<slug>. |
mandate_id_malformed | 400 | mandate_id_malformed | mandate_id did not match mnd_<ULID>. |
algorithm_unsupported | 400 | algorithm_unsupported | envelope.algorithm was not ed25519. |
replay_window_too_short | 400 | replay_window_too_short | Window did not cover the validity window. |
expired | 403 | expired | expires_at is in the past. |
issued_in_future | 403 | issued_in_future | issued_at exceeded the 60s skew tolerance. |
replay | 403 | replay | Nonce was already seen within its window. |
protocol_unsupported | 401 | identity_check_failed | Protocol not in the edge allowlist. |
agent_unknown | 401 | identity_check_failed | No registry entry for the agent_id. |
key_unknown | 401 | identity_check_failed | envelope.key_id did not match any eligible registered key. |
agent_revoked | 401 | identity_check_failed | Agent’s registry entry is revoked. |
signature_invalid | 401 | signature_invalid | ed25519 verify failed. |
delegation_chain_invalid | 401 | delegation_chain_invalid | A present chain failed contiguity, windows, scope, or link signatures. |
verification_unavailable | 503 | unavailable | Registry or nonce store could not answer. Fail-closed. |
The edge allowlist for protocols is currently [a2a, ap2, mcp]. ACP, UCP, and x402 are roadmap items; see protocols reference.
Verifying a mandate signature
Section titled “Verifying a mandate signature”A mandate signature can be verified end-to-end with off-the-shelf ed25519 tooling using the same recipe Sill uses for every other signed surface — fetch the JWKS, canonicalize the payload, reconstruct the domain-separated signing input, verify the ed25519 signature. The recipe is documented at Verify a signature. The mandate-specific differences:
- The signing input is
utf8("sill-mandate-v1") || 0x00 || utf8(JCS(signed)), not the JWS detached form used for the agent card and ARD trust manifest. - The verifying key is the agent’s registered ed25519 key (looked up as
(agent_id, key_id)against the identity registry), NOT Sill’s edge signing key.
Frequently asked
Section titled “Frequently asked”Does Sill ever sign the mandate itself? No. The agent signs the mandate with its own ed25519 key. Sill verifies the mandate against the agent’s registered public key, then signs the resulting audit record with its edge signing key.
Does the mandate carry the card or payment token?
No. The mandate carries max_amount, currency, and skill parameters — never PAN data. Payment authorization is handed to the merchant’s processor (Stripe today), which holds the card and authorizes the charge against a merchant-saved opaque token (pm_* or similar). Sill never custodies funds and never sees raw card data.
What protocols can the mandate ride on? Today, A2A (Google), AP2 (Google’s mandate-shaped extension), and MCP (Anthropic). The Agentic Commerce Protocol (ACP) (OpenAI + Stripe Shared Payment Tokens), the Universal Commerce Protocol (UCP) (Google + Shopify catalog), and x402 (Coinbase on-chain) are on the roadmap.
Can the same nonce be replayed across sites or agents?
No. The nonce is recorded against the (agent_id, nonce) pair and burned for replay_window_seconds. A replay within that window rejects with replay. The window cap is 600 seconds.
What happens if Sill’s verifier can’t answer?
The mandate rejects with verification_unavailable (HTTP 503). Sill is fail-closed by design: an unreachable registry or nonce store never produces an approval.
Is the mandate format compatible with AP2?
The protocol_context.protocol = "ap2" path carries AP2-shaped fields (cart_total, cart_currency, quoted_total) as additive optional fields inside the canonical signing input. The Sill mandate is the unified envelope; the boundary protocol is named in protocol_context.
See also
Section titled “See also”- Transactional overview — pipeline scope and honest bounds
- Policy engine — how rules drive approve/escalate/reject
- Human-in-the-loop — escalation review surface
- Payments — processor authorization
- Refunds — the refund mandate shape
- Verify a signature — verifier recipe for any Sill-signed surface
- Identifying agents — agent registry that supplies verifying keys
- Agent card — the signed identity surface
- Protocols reference — A2A, AP2, MCP, and the roadmap
- Security overview — fail-closed posture and threat model
- External: RFC 8785 JCS, RFC 8032 EdDSA, OWASP Top 10 for Agentic Applications, MITRE ATLAS, NIST AI RMF