Mandate and wallet kernel
Authority (what the agent may do) and the component that enforces it (the wallet). The core Stipendex principle: the LLM never touches money — see invariant I1 in overview.md.
Mandate — delegation of authority
JSON signed by the principal. Stored in the wallet; never disclosed to receivers (only its hash travels in a Proof).
{
"stipendex": "mandate/0.1",
"mandate_id": "m_7c1f…",
"principal_key": "ed25519:AAAA…",
"wallet_key": "ed25519:BBBB…",
"limits": {
"currency": "USD",
"per_tx": "1.00",
"per_day": "20.00",
"total": "100.00"
},
"allow": [
"registry:4agents/verified",
"mcp:exa-search",
"https://api.example.com"
],
"approval_over": "1.00",
"approval_mode": "async",
"valid_until": "2026-08-15T00:00:00Z",
"purpose": "research tools + purchases for project X",
"sig": "<principal JWS>"
}
| Field | Rules |
|---|---|
limits |
all three levels are mandatory; the wallet keeps counters durable (they survive restarts) |
allow[] |
trust sources: a direct receiver id, a URL, or a registry (registry:<provider>/<level>, see trust-registry.md). An empty list = nobody may be paid |
approval_over |
amounts above this require a Confirmation; 0 = confirm everything |
approval_mode |
interactive | async | deny — see HITL below |
valid_until |
SHOULD: days, not months — a short TTL instead of revocation infrastructure; early revocation via the facilitator |
purpose |
human-readable purpose; shown in every confirmation request |
There may be several mandates (per project/agent); the wallet selects by receiver and purpose, and on ambiguity picks the most restrictive.
Wallet kernel
A deterministic component at the agent host (a library, a sidecar process, or a remote service). Normative behavior:
The agent-facing interface — exactly one operation
wallet.pay(quote) → { status: "paid", proof }
| { status: "pending_approval", eta }
| { status: "denied", reason }
- No amount, receiver, or address parameters. The amount and receiver are defined by the Quote signed by the receiver — there is nowhere, and no reason, to trust the model's text.
- No other money operations reachable by the model MUST exist (transfer, top-up, key export, etc.). Prompt injection runs into the absence of an attack surface.
reasonindeniedis a machine-readable code (not_in_allowlist,per_day_exceeded,quote_expired…), so the agent can explain honestly to the principal.
Check order (MUST, exactly this order)
- Quote signature and expiry;
quote_idnot consumed before. The claim on a quote id is atomic; it is released again on any denial with no money movement (a legitimate retry stays possible) and becomes permanent once settlement succeeds. - Receiver ∈
allow, default deny. A direct entry (id/URL) admits a quote only whenquote.receiver.keymatches a key the wallet already trusts for that id (pinned in wallet config or fetched once from the receiver's descriptor) — otherwise any attacker could mint self-signed quotes under a trusted name (T1). Registry entries are checked against the provider-signed descriptor. - Limits:
per_tx→ daily counter →total. For anestimate— countestimate + tolerance(worst case). Passing the check RESERVES the amount atomically (no check-then-record window): the reservation is held through HITL waits, released on denial/failure, and kept as the spend record on success — concurrent payments and queued approvals cannot jointly overshoot the mandate. approval_overthreshold → HITL (below). Checkout profile — Confirmation always. After a long interactive wait the wallet re-checks quote expiry.- Rail selection (intersection of
quote.railsand configured adapters; priority from wallet config) → settlement → Proof.
Keys
- The wallet key lives outside the agent process (OS keychain, a separate daemon, KMS) — the process holding LLM context has no access to it. The wallet signs but holds no funds — funds live at the rail/facilitator.
- Host compromise is bounded by the mandate: the maximum damage = the remaining
limits until
valid_untilwithin the allowlist. This is a damage boundary the principal sets in advance, by design. - For server-hosted agents the same boundary applies remotely: keys MUST stay in custody the principal controls (their device, their KMS), and no operator-side service may be able to execute a payment unilaterally. This is not only a security property — it is the regulatory line that keeps the wallet non-custodial (outside money-transmitter / MiCA custody perimeters).
HITL — human confirmation
Three modes (approval_mode), one output object — Confirmation:
| Mode | Behavior |
|---|---|
interactive |
ask the principal in the current channel (MCP elicitation / CLI prompt); the answer is awaited synchronously |
async |
the payment goes to a pending queue (at the wallet or facilitator) + push to the principal; the agent gets pending_approval immediately and continues other work; queue TTL = Quote TTL |
deny |
headless-strict: everything above the threshold → denied; for cron agents with no channel to a human |
Confirmation — recorded consent
{
"stipendex": "confirmation/0.1",
"confirmation_id": "c_5e2b…",
"quote_id": "q_8f3d1c…",
"mandate_id": "m_7c1f…",
"approved_total": { "value": "92.40", "currency": "USD" },
"tolerance_ack": true,
"channel": "elicitation",
"approved_at": "2026-07-16T12:03:02Z",
"sig": "<wallet JWS>",
"principal_assertion": "<webauthn/passkey assertion | null>"
}
- Base level: signed by the wallet, recording the channel, the text shown, and the answer.
- Strong level (SHOULD for checkout):
principal_assertion— a passkey/WebAuthn signature by the principal over the confirmation digest. This is machine-readable proof that a specific human accepted a specific amount — what gets presented to a bank in a chargeback, closing the "agent authority" legal gap (risk H5 from the 4agents order-flow analysis). The direction is compatible with AP2/FIDO. - The confirmation is bound to
quote_id: a "yes" to one total cannot be reused for another.
What the wallet gives the principal
- A journal:
mandate → confirmation → quote → proof → receiptper operation. - Real-time limit counters ("$3.20 of $20 today").
- The pending queue with approve/deny buttons.
- Per-operation evidence chain export (for disputes/chargebacks).