Stipendex

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 }

Check order (MUST, exactly this order)

  1. Quote signature and expiry; quote_id not 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.
  2. Receiver ∈ allow, default deny. A direct entry (id/URL) admits a quote only when quote.receiver.key matches 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.
  3. Limits: per_tx → daily counter → total. For an estimate — count estimate + 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.
  4. approval_over threshold → HITL (below). Checkout profile — Confirmation always. After a long interactive wait the wallet re-checks quote expiry.
  5. Rail selection (intersection of quote.rails and configured adapters; priority from wallet config) → settlement → Proof.

Keys

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>"
}

What the wallet gives the principal