Zado Agent Trust Protocol v0.2
The consumer-side policy layer for AI agents that spend real money.
v0.2 — supersedes v0.1 (2026-04-25). Date: 2026-05-03. Corresponds to
zado-mcp >= 0.2.0, < 0.3.0.
| Field | Value |
|---|---|
| Version | 0.2 |
| Status | Public draft |
| Date | 2026-05-03 |
| Supersedes | v0.1 (2026-04-25) — preserved unchanged for citation continuity |
| Reference implementation | zado-mcp >= 0.2.0, < 0.3.0 (PyPI) |
| Author | Evolving Intelligence AI, LLC |
| License (spec) | CC-BY 4.0 |
| License (reference implementation) | See project LICENSE |
| Canonical URL | https://zadofi.ai/protocol/zado-agent-trust-protocol-v0.2 |
Provenance
This specification describes behavior that is shipped and operating in the
Zado backend and the packaged zado-mcp PyPI distribution as of the
publication date. It is not aspirational. Every normative claim traces to a
backing source file; the publicly-verifiable subset is enumerated in
Appendix A — Source-of-truth traceability (PyPI-distributed surfaces
only, since the backend repository is currently private).
The protocol was first exercised end-to-end on 2026-04-03, when an
OpenClaw agent registered a Zado spend-scoped token, called check_budget,
called authorize_purchase(43.20, "groceries", "Whole Foods") and received an
authorized: true response with a transaction id and remaining envelope
balance. The agent had no access to the user's bank credentials, account
numbers, or transactions outside its bound category. That run is the existence
proof for v0.1 — the v0.1 trust surface is preserved verbatim in v0.2.
The Human Approval Gate (HAG) — the deferral lifecycle that turns the v0.1
"policy gate" into a true two-phase delegated spend protocol — was first
exercised end-to-end in HAG Phase 1.7 (2026-05-03) when an agent's
authorize_purchase request above threshold parked into
pending_authorizations, the user approved via the desktop UI, and the agent
explicitly claimed the approval via complete_pending_authorization. The
authorize-then-claim pattern is normative in v0.2 (see §3.11).
Items labeled "v0.3+ roadmap" in §9 are explicitly NOT shipped today. Integrators MUST NOT depend on roadmap items. (v0.2 promotes from "v0.2+ roadmap" → normative the surfaces marked in the "Changes from v0.1" section below; everything else from v0.1's roadmap remains future work.)
Changes from v0.1
This section is informative. It enumerates the normative changes between v0.1 (2026-04-25) and v0.2 (2026-05-03). v0.2 is fully backwards-compatible with v0.1 — a v0.1-targeted integrator continues to work unchanged. The promotions add capability without removing or renaming any documented surface.
- §3.11 Human Approval Gate promoted from "v0.2+ roadmap" to normative.
The full deferral lifecycle is documented:
pending → approved → completedplus terminaldeniedandexpiredpaths. The authorize-then-claim pattern is named (mapping to payments-industry "auth/capture") and the two-tool agent claim flow (check_pending_authorization+complete_pending_authorization) is normative. - §4 MCP tool inventory expanded from 4 to 6 tools. The two new tools
are
check_pending_authorizationandcomplete_pending_authorization. They are the load-bearing additions that turn v0.1's synchronous policy gate into a true two-phase protocol.list_envelopes(already shipped in v0.1 in practice but documented thinly) is now documented at the same depth as the other tools. - §7.1 install instructions canonicalized.
pip install zado-mcpis the primary install path; the canonical MCP config block usescommand: "zado-mcp", args: ["serve"]. The legacypython -m app.mcp.serverform is demoted to a developer / local-checkout alternative. completion_metadatadocumented as a normative observability surface. The JSON shape{transaction_ledger_entry_id, envelope_id_at_debit, debited_amount, completed_at, envelope_remaining_at_debit}links the human's "yes" to the actual ledger debit. Third parties reconciling Zado-mediated agent spend can anchor on this field.- Per-tool auth boundaries made explicit. Each MCP tool entry in §4 states who can call it: agent token (MCP), human session (REST), or both. Cross-boundary calls (e.g., a human session calling an agent-only MCP tool) return 403.
- §6 Security elevated to a three-part normative foundation: threat model, blast radius, and reversibility / audit. v0.1 covered defense in depth in passing; v0.2 answers the gating adoption question — "what's the worst that can happen if my Zado-issued agent token is compromised?" — directly.
- §8 AP2 mapping reframed: Zado IS an AP2 v0.2 Trusted Surface. v0.1 said "Zado is in the same space as AP2"; v0.2 names Zado's role formally in the AP2 v0.2 vocabulary (Trusted Agent Provider model). The mapping table uses AP2 v0.2 terminology (Open Mandate, Closed Mandate, Mandate Receipt) and explicitly marks Checkout / Payment Mandate as v0.4+ scope.
- Appendix A — Source-of-truth traceability added. Citations use
file-plus-symbol form (line numbers go stale; symbols are stable).
Scoped to publicly-distributed code (PyPI
zado-mcppackage) since the backend repository is currently private.
1. Introduction
Zado is a policy and permission layer for AI agents that need to make purchases on behalf of a human. It sits above a person's real bank accounts (connected via Plaid) and below the agent-payment rails being built by the card networks.
When an AI agent wants to spend money, three questions must be answered:
- Who said it could? — identity and scope (Zado:
agent_token.scope) - How much can it spend, on what? — policy and budget (Zado: envelopes, caps, binding, pacing)
- How does the money actually move? — settlement (Zado: today, a ledger entry; in v0.2+, optionally a Stripe Issuing one-time virtual card)
Most existing agent-payment work focuses on question 3. Zado focuses on questions 1 and 2 — the policy gate that any settlement rail can consult before money moves.
The spec is intentionally compatible with, and complementary to:
- Google AP2 v0.2 (Agent Payments Protocol) — Zado fulfills AP2 v0.2's
Trusted Surface role in the Trusted Agent Provider model. The agent
token corresponds to an AP2 Open Mandate; the pending authorization to a
Closed Mandate;
completion_metadatato the local-evidence equivalent of a Mandate Receipt. (Full mapping in §8.1.) - Stripe Machine Payments Protocol (MPP) — Zado is the consumer-side policy layer an MPP-scoped agent can consult before invoking MPP-routed payment.
- Visa Intelligent Commerce / Mastercard Agent Pay — Zado is the consumer-side budget gate that complements network-side spend controls and tokenization.
- Model Context Protocol (MCP) — Zado exposes its policy surface via six MCP tools (the four v0.1 tools plus the two HAG tools added in v0.2).
What Zado is not:
- Zado is not a money transmitter. It does not move funds.
- Zado is not a card issuer in v0.2. (v0.3+ may issue one-time virtual cards via Stripe Issuing — Stripe remains the regulated entity.)
- Zado is not a replacement for AP2, MPP, Visa IC, or MC Agent Pay. It is a complement to them.
2. Positioning
Zado occupies a layer that, before this spec, had no public name.
| Player | Layer | Primary Consumer | Spend Authority | Settlement |
|---|---|---|---|---|
| Zado Agent Trust Protocol (this spec) | Consumer policy / permission gate (AP2 v0.2 Trusted Surface role) | End user (with ADHD-friendly UX) | Envelope balance + agent token scope + caps + binding + pacing + HAG threshold | None today (ledger entry + completion_metadata); Stripe Issuing virtual card in v0.3+ |
| Google AP2 v0.2 | Open agent-payment protocol (rails-agnostic) | Merchants + agents + payment processors | Open Mandate + Closed Mandate + Mandate Receipt (cryptographic VDCs) | Pluggable (cards, wallets, instant rails) |
| Stripe MPP | Agent-payments protocol on Stripe rails | Stripe-integrated merchants | Stripe-issued credentials, scope-bound | Stripe rails |
| Visa Intelligent Commerce Connect | Network-side AI payment platform | Visa-accepting merchants | Tokenization + spend controls per agent | Visa network |
| Mastercard Agent Pay | Network-side agentic payment program | All Mastercard cardholders | Mastercard Agentic Tokens, consumer-set scopes | Mastercard network |
The pattern: card networks and protocol-level players answer how money moves once authorized. Zado answers what an agent is allowed to spend, on behalf of which envelope, against which budget, in the first place. A Visa-Intelligent-Commerce-routed payment authorized through MPP can ask a Zado-equivalent policy layer "is this purchase permitted by the user's budget?" before it ever touches the network. Today that consultation is done inside each card-network's own platform; Zado is the first open, MCP-native, real-bank-account-backed implementation of that consumer-side gate.
The single-sentence positioning: Zado is the consumer-side budget gate for AI agents — the layer that says "yes" or "no" to a spend before any settlement rail moves money.
3. Core concepts
This section is normative. Each term is defined once. Every term cites the source file that implements it.
3.1 Envelope
A named pool of money allocated to a spending category for a calendar month.
"Groceries: $400 budgeted, $123.50 spent, $276.50 remaining." The envelope
balance is the hard ceiling for any agent or human spend in that category.
Source: backend/app/models/envelope.py and backend/app/services/envelope.py.
3.2 Envelope-as-Permission
The fundamental design pattern of this protocol: the envelope balance IS the
spending limit for any agent bound to that envelope. No separate
"agent budget" exists. There is only the user's envelope, which the agent may
draw against subject to scope and guards. When an agent asks "can I spend
$43?", the answer is computed from the same envelope balance the user sees in
the app. There are no parallel ledgers and no agent-only allowances.
Source: backend/app/services/agent_spending.py (the authorize_purchase
flow checks the live envelope via SpendingService.check_can_afford).
3.3 Agent Token
A SHA-256-hashed bearer credential issued to a single AI agent on behalf of a
single user. Token plaintext is shown once at creation and never stored.
Tokens carry: id, user_id (FK), name, scope, is_active, spending
guards, optional allowed_category_ids, optional pace_multiplier, optional
expires_at. Tokens are revocable individually or in bulk (the kill switch).
Source: backend/app/models/agent_token.py.
3.4 Scope
A token has exactly one of two scopes:
read— May call read-only tools (check_budget,list_envelopes,get_daily_status). Cannot authorize purchases.spend— May call all read-only tools ANDauthorize_purchase. Subject to all spending guards.
Scope is checked server-side as the first gate in authorize_purchase.
Source: backend/app/services/agent_spending.py.
3.5 Envelope Binding
A token MAY be bound to a specific set of envelopes via allowed_category_ids
(a JSON array of stable category UUIDs). When bound, the agent can only
authorize purchases against those categories. An attempt to spend in any other
category is rejected with envelope_not_bound. A null binding means the agent
may spend in any of the user's envelopes (subject to all other guards).
Legacy allowed_category_slugs is honored for backwards compatibility.
Source: backend/app/services/agent_guard.py (check_envelope_binding).
3.6 Per-Transaction Cap
A hard maximum dollar amount per single authorize_purchase call,
configurable per token (default $50). Exceeding it returns
per_transaction_cap_exceeded. Source: backend/app/services/agent_guard.py.
3.7 Session Cumulative Cap
A rolling total spending limit across all of an agent's purchases since the
last session reset (default $100). Exceeding it returns
session_cap_exceeded. The session auto-resets after 24 hours of
inactivity to prevent permanent lockout from a one-time spike.
Source: backend/app/services/agent_guard.py (SESSION_RESET_SECONDS).
3.8 Rate Limit
A maximum of three authorize_purchase calls per minute per agent. Exceeding
it returns rate_limited with a retry_after_seconds field. The minute
window is sliding, anchored to last_transaction_at.
Source: backend/app/services/agent_guard.py (AGENT_RATE_LIMIT).
3.9 Budget Pacing
A guard that rejects purchases consuming budget faster than a sustainable
daily pace. Computed as daily_pace = envelope_remaining / days_remaining,
then pace_limit = daily_pace * pace_multiplier. Default
pace_multiplier = 3.0. Exceeding the pace returns exceeds_budget_pace
with the computed daily_pace, pace_limit, and days_remaining.
Source: backend/app/services/agent_guard.py (check_budget_pace).
3.10 Kill Switch
A single user-initiated action that revokes ALL of a user's agent tokens
simultaneously (POST /agents/revoke-all, exposed in the desktop app under
Settings → Agent Access → Freeze All Agents). Revoked tokens immediately
return 401 on next use. Designed for the "something is wrong, stop everything
right now" moment. Source: backend/app/api/agents.py.
3.11 Human Approval Gate
The Human Approval Gate (HAG) is the deferral lifecycle that turns the v0.1
synchronous policy gate into a true two-phase delegated spend protocol. When
an agent's authorize_purchase request meets the per-token
requires_human_approval_threshold, the request is parked as a
pending_authorization row in status pending, the agent receives a
deferral response with a next_action hint, and the user is invited back in
to approve or deny via the desktop UI. After approval, the agent explicitly
claims the approval by calling complete_pending_authorization — only at
claim time does the envelope debit + transaction ledger entry happen.
This section is normative.
3.11.1 The authorize-then-claim pattern
The pattern maps to payments-industry vocabulary:
- Authorize (
authorize_purchase) — policy evaluation. Either auto-approves (and writes the transaction immediately, just like v0.1) or defers to human review (writes apending_authorizationrow, returns no transaction). - Claim (
complete_pending_authorization) — agent-initiated capture of an approved authorization. Atomic CAS update flips the row fromapprovedtocompleted, debits the envelope, writes the transaction ledger entry, and populatescompletion_metadata.
This is functionally equivalent to "auth/capture" on a card rail. The envelope
is not debited at the human's "yes"; it's debited at the agent's claim.
That separation makes the protocol observable and idempotent: a human approval
without a subsequent agent claim is a real, auditable event ("user said yes
but the agent never followed up") and a re-claim of the same pending_id is
a no-op that returns the cached completion record.
3.11.2 Threshold semantics
The per-token requires_human_approval_threshold (numeric, default null)
controls when HAG fires:
| Threshold value | Behavior | Example |
|---|---|---|
null (default) |
HAG OFF. All purchases auto-approve up to per-transaction cap and other guards. This is the v0.1 behavior and is the default for backwards compatibility. | A research agent token with threshold = null and per_tx_cap = $50: a $15 purchase auto-approves; a $60 purchase rejects with per_transaction_cap_exceeded. |
0 |
HAG ALWAYS-ASK. Every authorize_purchase call parks for human review, regardless of amount. |
A new agent on a sensitive envelope with threshold = 0: a $5 snack purchase parks; the user sees it in the inbox before any debit. |
$X (positive decimal) |
HAG ASK-AT-OR-ABOVE-X. Purchases >= $X park; purchases < $X auto-approve up to per-transaction cap. The bound is inclusive — amount == $X parks. |
A grocery agent with threshold = $40: a $32 purchase auto-approves; a $40 purchase parks; a $45 purchase parks. |
Terminology: opt-in HAG (default off) — the system ships HAG-disabled so v0.1 integrators see no behavior change. (Distinct from "default deny" — see the user-facing setup guidance.)
The threshold is validated at agent registration against the resolved
per_transaction_cap: a threshold above the per-tx cap is silently
unreachable (Guard 1 rejects the amount before HAG ever fires), so the API
returns 422 rather than accepting dead state. Read-scope agents cannot
spend, so any threshold is null'd out at the request boundary.
3.11.3 Status enum
A pending_authorization row carries one of five status values. These are
the exact lowercase strings wire-protocol clients see in
check_pending_authorization responses and in the current_status field of
complete_pending_authorization error envelopes:
| Status | Meaning | Terminal? |
|---|---|---|
pending |
Parked, awaiting user decision (default expiry: 15 minutes from request). | No |
approved |
User approved via desktop UI / REST. Awaiting agent claim. | No (agent must claim before expires_at) |
denied |
User explicitly denied. | Yes |
expired |
The 15-minute total budget on expires_at passed before the agent claimed (covers both "user never decided" AND "user said yes but agent didn't follow up"). |
Yes |
completed |
Agent claimed an approved authorization; envelope debit + transaction written. | Yes |
3.11.4 Deferral lifecycle
┌──────────────┐
authorize_purchase │ │
(amount >= threshold) │ denied │
│ ┌──── user denies via UI/REST ─┤ (terminal) │
▼ │ │ │
┌────────────┐ │ └──────────────┘
│ pending │────────────────────┤
│ │ │ complete_pending_authorization
└────────────┘ │ (CAS: approved → completed)
│ │ + envelope debit
│ user approves ▼ + transaction ledger entry ┌──────────────┐
│ via UI/REST ┌────────────┐ │ │
└──────────────────▶│ approved │───────────────────────────────────▶│ completed │
│ │ │ (terminal) │
└────────────┘ │ │
│ └──────────────┘
│ (no claim before expires_at) ▲
▼ │
┌────────────┐ │
│ expired │◀──────── (no decision before ─────────┘
│ (terminal) │ expires_at, from `pending`)
└────────────┘
Numbered flow:
- Agent calls
authorize_purchase. Amount meets threshold → row written withstatus = pending,expires_at = requested_at + 15 minutes. Response includesreason: "pending_human_approval",pending_id,expires_at, and anext_actionhint pointing the agent atcheck_pending_authorizationandcomplete_pending_authorization. - Agent polls
check_pending_authorization(pending_id). The service inline-expires any stale rows (covers bothpendingandapprovedpastexpires_at) so a polling client never sees a status that should already be terminal. - User opens the desktop app, sees the request in the Pending
Authorizations inbox, taps Approve or Deny. (Or calls
POST /pending-authorizations/{id}/approve|denydirectly.) The row transitionspending → approvedorpending → denied. Approval does not debit the envelope; it just unlocks the claim window. - Agent calls
complete_pending_authorization(pending_id). The service runs an atomic CAS UPDATE that flipsapproved → completedonly if the row is stillapprovedAND not yet expired. On CAS success: envelope is debited via the samequick_spendpath used by auto-approved purchases, transaction ledger entry is written,completion_metadatais populated, and the response shape matches an auto-approvedauthorize_purchasesuccess. - Terminal alternatives:
- User denies in step 3 → row sits at
denied; agent's claim returns409 invalid_state. - Nobody acts before
expires_at→ row flips toexpiredon the next read; agent's claim returns410.
- User denies in step 3 → row sits at
3.11.5 completion_metadata (normative observability surface)
Populated on the agent's claim. Shape:
{
"transaction_ledger_entry_id": "uuid",
"envelope_id_at_debit": "uuid",
"debited_amount": "decimal-string",
"completed_at": "ISO-8601 UTC",
"envelope_remaining_at_debit": "decimal-string"
}
Field semantics:
transaction_ledger_entry_id— UUID of theTransactionrow written by the claim. Resolves to a single, auditable ledger entry that any third party reconciling Zado-mediated agent spend can anchor on. The same id appears in the audit log entry for the debit and in the agent activity feed event withoutcome="completed",reason_code="human_approval_redeemed".envelope_id_at_debit— UUID of the envelope row mutated by the debit. May benullif the bound category exists but has no envelope budgeted in the effective month (the spend is still recorded, with0envelope context).debited_amount— decimal string of the amount actually debited; equals the amount on the originalauthorize_purchasecall.completed_at— ISO-8601 UTC timestamp of the CAS that flippedapproved → completed.envelope_remaining_at_debit— decimal string of the envelope balance immediately after the debit. This is the same post-claim value returned asenvelope_remainingin the success response, cached here so idempotent replays can return the same balance without re-running the debit path.
This field is the audit-trail anchor that links a specific human "yes" to a specific ledger debit. It is the single source of truth for agentic spend reconciliation in v0.2.
3.11.6 Idempotent replay
A successful complete_pending_authorization call is idempotent on the
pending_id. Re-calling on a row already in status completed returns the
same response shape (same transaction_id, same amount, same
envelope_remaining) by reading the cached completion_metadata — no
second debit, no second activity event, no second audit row. This makes the
claim safe to retry across network failures, agent restarts, or duplicated
prompts.
3.11.7 Error semantics
complete_pending_authorization maps to HTTP status codes:
| Status | Trigger | Response shape |
|---|---|---|
200 |
First successful claim, OR idempotent replay of a prior completed row. |
{authorized: true, transaction_id, amount, category, vendor, envelope_remaining, pending_id} |
404 |
pending_id does not exist, OR belongs to a different user, OR (per agent-isolation rules) belongs to a different agent token under the same user. The 404 is uniform across all three to avoid leaking existence. |
{status: "not_found"} |
409 |
Row exists and is reachable, but status != approved (caller error: still pending, or already denied). The body carries the actual current status. |
{status: "invalid_state", current_status, reason: "pending_status_invalid", message} |
410 |
Row was approved but the 15-minute window passed before the agent claimed, OR the agent token was deleted between approve and claim. The row has been flipped to expired by the call itself; subsequent claims see expired directly. |
{status: "expired", reason, message} |
Network or transport-level errors at the MCP layer surface as
{error: "<detail>"} and are NOT policy decisions — agents SHOULD treat them
as retryable infrastructure faults.
3.12 Audit Context
Every mutation initiated by an agent is automatically tagged with
actor_type="mcp_agent" and actor_details containing the agent's id, name,
scope, and current session_spend_so_far. Capture is automatic via
AuditMiddleware — no per-route plumbing required.
Source: backend/app/core/audit_context.py and backend/app/middleware/audit.py.
4. MCP tools
This section is normative. Zado exposes six Model Context Protocol tools via
the packaged zado-mcp PyPI distribution. The server runs locally over stdio;
all authentication, scope enforcement, guard checks, HAG state machine, audit
logging, and database access happen server-side at the Zado backend
(zadofi.ai by default). The local MCP process is a pure transport layer and
cannot bypass policy.
AI Agent → MCP stdio → ZadoAPIClient → HTTPS Bearer → Zado backend → policy + envelopes + HAG
The six tools split across two scopes (see §3.4):
| Tool | Scope | Phase |
|---|---|---|
check_budget |
read |
Read |
list_envelopes |
read |
Read |
get_daily_status |
read |
Read |
authorize_purchase |
spend |
Phase 1 (authorize) |
check_pending_authorization |
spend |
Phase 1 ↔ 2 (poll for human decision) |
complete_pending_authorization |
spend |
Phase 2 (claim) |
Auth boundaries. Each tool entry below states explicitly who can call it:
- Agent token (MCP) — invoked over MCP stdio with a Bearer agent token. Subject to scope, binding, and guard checks per §3 and §6.
- Human session (REST) — invoked over HTTPS by the desktop app with the user's session cookie. Subject to CSRF + auth.
- Both — both surfaces are valid; per-route logic distinguishes actor type.
Cross-boundary calls (a human session attempting an agent-only MCP tool, or
an agent token attempting a human-only REST endpoint) are rejected with
403. The agent-side allowlist is enforced by AgentScopeMiddleware on the
backend (default-deny — new endpoints require explicit addition).
4.1 check_budget
Scope required: read
Auth boundary: Agent token (MCP) only. The HTTP backing route is on the
agent allowlist; a human session calling the same MCP tool name has no
analog (humans use the desktop app's spending views directly).
HTTP backing route: GET /api/spending/category/{category}
Returns the budgeted, spent, and remaining amounts for a single envelope.
Request schema:
| Field | Type | Required | Description |
|---|---|---|---|
category |
string | yes | Category slug (e.g., "groceries", "dining", "fun-money") |
Success response schema:
| Field | Type | Description |
|---|---|---|
category |
string | Human-readable envelope name |
remaining |
number | Available balance in dollars |
budgeted |
number | Monthly budgeted amount in dollars |
spent |
number | Amount spent so far this month in dollars |
percentage_used |
number | spent / budgeted * 100 |
Example request:
{ "category": "groceries" }
Example success response:
{
"category": "Groceries",
"remaining": 276.50,
"budgeted": 400.00,
"spent": 123.50,
"percentage_used": 30.875
}
4.2 list_envelopes
Scope required: read
Auth boundary: Agent token (MCP) only. The HTTP backing route is on the
agent allowlist; humans see the same data in the desktop app's envelopes view.
HTTP backing route: GET /api/envelopes/summary?month=YYYY-MM
Returns all envelope balances for the current calendar month.
Request schema: No parameters. The MCP tool implicitly uses the current
month in YYYY-MM form (UTC).
Success response schema:
| Field | Type | Description |
|---|---|---|
month |
string | YYYY-MM of the month being reported |
total_budgeted |
number | Sum of all envelope budgets |
total_spent |
number | Sum of all envelope spending |
total_available |
number | total_budgeted - total_spent |
envelopes |
array | Per-envelope detail (see below) |
Each envelopes[] entry:
| Field | Type | Description |
|---|---|---|
name |
string | Envelope name |
budgeted |
number | This envelope's budget |
spent |
number | This envelope's spending |
remaining |
number | This envelope's available balance |
percentage_used |
number | spent / budgeted * 100 |
status |
string | Envelope health status (e.g., "on_track", "warning", "empty") |
Example success response:
{
"month": "2026-04",
"total_budgeted": 2400.00,
"total_spent": 1820.30,
"total_available": 579.70,
"envelopes": [
{
"name": "Groceries",
"budgeted": 400.00,
"spent": 123.50,
"remaining": 276.50,
"percentage_used": 30.875,
"status": "on_track"
},
{
"name": "Dining",
"budgeted": 200.00,
"spent": 198.00,
"remaining": 2.00,
"percentage_used": 99.0,
"status": "warning"
}
]
}
If the agent token has allowed_category_ids set, the response is filtered
to only those envelopes (server-side, via
backend/app/services/agent_guard.py:filter_by_binding).
4.3 get_daily_status
Scope required: read
Auth boundary: Agent token (MCP) only. The HTTP backing route is on the
agent allowlist.
HTTP backing route: GET /api/spending/status
Returns today's spending posture across all envelopes — total available, daily allowance, and any active alerts.
Request schema: No parameters.
Success response schema:
| Field | Type | Description |
|---|---|---|
total_available |
number | Sum across all envelopes (or bound envelopes) for current month |
daily_allowance |
number | Sustainable daily spend rate for the rest of the month |
days_remaining |
number | Days left in the current month (including today) |
alerts |
array | Active spending alerts |
Each alerts[] entry:
| Field | Type | Description |
|---|---|---|
category |
string | Envelope name the alert targets |
type |
string | Alert type (e.g., "envelope_empty", "pace_warning", "upcoming_bill") |
message |
string | Human-readable alert text |
Example success response:
{
"total_available": 579.70,
"daily_allowance": 96.62,
"days_remaining": 6,
"alerts": [
{
"category": "Dining",
"type": "pace_warning",
"message": "You're spending faster than the envelope allows for the rest of the month."
}
]
}
4.4 authorize_purchase
Scope required: spend
Auth boundary: Agent token (MCP) only. Humans do NOT call this — humans
spend through the desktop app's normal transaction-entry surface.
HTTP backing route: POST /api/agents/purchase
Authorizes and logs a purchase if and only if the envelope has enough funds AND all guards pass. This is the load-bearing tool of the spec — it implements the envelope-as-permission pattern end-to-end.
In v0.2, authorize_purchase becomes the Phase 1 (authorize) step of the
authorize-then-claim pattern when the agent token has
requires_human_approval_threshold set and the amount meets it. See §3.11
for the deferral lifecycle. When HAG is off (threshold = null, the v0.1
default), authorize_purchase behaves identically to v0.1 — synchronous
debit + transaction creation on success.
Request schema:
| Field | Type | Required | Description |
|---|---|---|---|
amount |
number | yes | Purchase amount in dollars (e.g., 43.20) |
category |
string | yes | Category slug — must match an envelope and (if bound) the token's allowed_category_ids |
vendor |
string | yes | Merchant or vendor name (free text, e.g., "Whole Foods") |
Authorization flow (from services/agent_spending.py::authorize_purchase):
- Scope check. Token must have
scope == "spend". Otherwise reject withinsufficient_scope. - Envelope binding check. If the token has
allowed_category_ids, the request's category must resolve to one of them. Otherwise reject withenvelope_not_bound. - Guards. In order: per-transaction cap, session cumulative cap, rate limit, budget pacing. Any failure rejects with the corresponding code.
- Envelope balance check. The envelope must have enough remaining funds
for the purchase. Otherwise reject with
envelope_empty. - Human Approval Gate (HAG, v0.2). If the token has
requires_human_approval_thresholdset andamountmeets it, defer: roll back the guard-state mutations from step 3, write apending_authorizationrow, returnreason: "pending_human_approval"with apending_id,expires_at, and anext_actionhint pointing the agent atcheck_pending_authorizationandcomplete_pending_authorization. See §3.11. When the threshold isnull(the v0.1 default), this step is a no-op and execution falls through. - Atomic commit. Guard state update + transaction creation + envelope-balance update commit together. Any exception rolls back all three.
Success response schema:
| Field | Type | Description |
|---|---|---|
authorized |
boolean | Always true for this shape |
transaction_id |
string | UUID of the created transaction |
amount |
number | Echoed authorized amount |
category |
string | Echoed category slug |
vendor |
string | Echoed vendor name |
envelope_remaining |
number | Envelope balance AFTER the purchase |
Example success response:
{
"authorized": true,
"transaction_id": "8f2a1e3c-7b40-4d8e-9c71-c1d2e3f4a5b6",
"amount": 43.20,
"category": "groceries",
"vendor": "Whole Foods",
"envelope_remaining": 233.30
}
Rejection response schema:
| Field | Type | Description |
|---|---|---|
authorized |
boolean | Always false for this shape |
reason |
string | One of the rejection codes below |
detail |
object | string | Code-specific context (see below) |
Rejection codes:
| Code | Triggered by | Context fields | Recommended agent retry |
|---|---|---|---|
insufficient_scope |
Token has scope != "spend" |
detail (string explanation) |
Do NOT retry. Re-register the agent with scope: "spend" and a fresh token. |
envelope_not_bound |
Request category not in token's allowed_category_ids |
category, bound_category_ids (array) |
Do NOT retry the same category. Either rebind the token to include this category, or use a different category that IS bound. |
per_transaction_cap_exceeded |
amount > token.per_transaction_cap |
limit (the cap, in dollars) |
Do NOT retry the same amount. Split the purchase into multiple smaller calls (each ≤ cap), or raise the cap. |
session_cap_exceeded |
session_spending_total + amount > token.session_spending_cap |
limit (cap), session_total (current spent) |
Do NOT retry. Wait for the 24-hour session reset, or have the user raise the session cap. |
rate_limited |
More than 3 calls in the last 60 seconds | limit (3), retry_after_seconds (int) |
Wait retry_after_seconds then retry. |
exceeds_budget_pace |
Purchase consumes more than pace_multiplier × daily_pace of the envelope |
daily_pace, pace_limit, days_remaining, envelope_remaining, pace_multiplier |
Either retry with a smaller amount ≤ pace_limit, or have the user raise pace_multiplier. |
envelope_empty |
Envelope remaining < amount after all prior guards passed |
detail (recommendation string) |
Do NOT retry. Either pick a different category or have the user fund the envelope. |
pending_human_approval |
HAG triggered (amount >= requires_human_approval_threshold); request parked for human review (v0.2). |
pending_id, expires_at, amount, category, vendor, next_action: {poll, when_approved, pending_id} |
Do NOT retry authorize_purchase — that would create a duplicate pending row. Poll check_pending_authorization(pending_id) until status = approved or terminal; on approved, call complete_pending_authorization(pending_id) to claim. |
A transport-level error (network failure, malformed response from the API)
returns reason: "api_error" — this is not a policy decision and the
agent SHOULD treat it as a retryable infrastructure fault.
Example rejection:
{
"authorized": false,
"reason": "exceeds_budget_pace",
"detail": {
"allowed": false,
"reason": "exceeds_budget_pace",
"daily_pace": 17.16,
"pace_limit": 51.49,
"days_remaining": 6,
"envelope_remaining": 102.97,
"pace_multiplier": 3.0
}
}
4.5 check_pending_authorization
Scope required: spend (when invoked by an agent token)
Auth boundary: Dual-surface — agent token via MCP (spend scope) AND
human session via REST (read scope). Both surfaces return the same
response shape today; the route is allowlisted on both sides because the
data (own pending status) is safe for the calling user to read either way.
HTTP backing route: GET /api/agents/pending-authorizations/{pending_id}
⚠ Future maintainers: any new field added to this response MUST be classified as agent-only-readable or human-readable. Agent-only fields require splitting this endpoint into two distinct routes; do NOT mix surfaces in one response. Adding agent-only data to a dual-surface response is a data-exposure path.
Polls the status of a parked agent purchase request. When authorize_purchase
returns reason: "pending_human_approval", the agent uses this tool to learn
whether the user approved, denied, or let the request expire — without
short-polling the human (the human gets a notification via the desktop app).
The service inline-expires stale rows on every read, so a polling client
always sees a fresh status. A row that should already be expired is
flipped to expired before the response is built; the agent will never see
a stale pending or approved past expires_at. (See §3.11 for status enum.)
Cross-agent isolation. A pending row is owned by the agent token that
created it. An agent token attempting to read another agent's pending row
under the same user receives 404 (not 403) so the agent cannot enumerate
other agents' activity by probing.
Request schema:
| Field | Type | Required | Description |
|---|---|---|---|
pending_id |
string (UUID) | yes | The pending_id returned by authorize_purchase |
Success response schema:
| Field | Type | Description |
|---|---|---|
pending_id |
string | UUID of the pending row |
status |
string | One of pending, approved, denied, expired, completed (the wire literals from §3.11) |
amount |
number | Amount of the original request, in dollars |
category |
string | Echoed category slug |
vendor |
string | null | Echoed vendor name (may be null if the original request omitted it) |
requested_at |
string | ISO-8601 UTC timestamp of the original request |
expires_at |
string | ISO-8601 UTC timestamp at which the row terminates if not claimed |
resolved_at |
string | null | ISO-8601 UTC timestamp of the user's approve/deny, or null if still pending |
resolution_note |
string | null | Optional free-text note attached by the user at approve/deny time |
Not-found response:
{ "status": "not_found" }
Returned when the pending_id does not exist, OR belongs to a different user
(cross-tenant probe), OR belongs to a different agent under the same user
(cross-agent probe). Uniform 404 to avoid existence leak.
Example success response (status approved):
{
"pending_id": "8b2c4d6e-3f1a-4d5b-9e7c-1a2b3c4d5e6f",
"status": "approved",
"amount": 87.50,
"category": "groceries",
"vendor": "Whole Foods",
"requested_at": "2026-05-03T18:42:11.382Z",
"expires_at": "2026-05-03T18:57:11.382Z",
"resolved_at": "2026-05-03T18:43:55.211Z",
"resolution_note": null
}
When the response shows status: "approved", the agent's next action is
complete_pending_authorization(pending_id).
4.6 complete_pending_authorization
Scope required: spend
Auth boundary: Agent token (MCP) only. The complete endpoint is not
exposed to human sessions — humans approve via
POST /pending-authorizations/{id}/approve, which only changes the row's
status to approved. The actual envelope debit + transaction write happens
when the agent claims via this MCP tool. A human session calling the
/api/agents/pending-authorizations/{id}/complete endpoint receives 403.
HTTP backing route: POST /api/agents/pending-authorizations/{pending_id}/complete
Phase 2 of the authorize-then-claim pattern. Atomically claims a
user-approved pending authorization: the service runs a CAS UPDATE that flips
approved → completed only if the row is still approved AND not yet expired,
then debits the envelope through the same quick_spend path used by
auto-approved purchases. The Transaction shape, source, agent_token_id, and
envelope-update SQL match the auto-approved path exactly, so nightly
self-healing sees a consistent ledger.
The CAS pattern guarantees exactly-one debit under concurrent claims —
losers see rowcount = 0 and route into the appropriate error response based
on the row's actual state.
Idempotent. Re-calling on a row already in status completed returns the
same response shape (same transaction_id, same envelope state) by
reading the cached completion_metadata — no second debit, no second activity
event, no second audit row. Safe to retry.
Request schema:
| Field | Type | Required | Description |
|---|---|---|---|
pending_id |
string (UUID) | yes | The pending_id returned by authorize_purchase (must currently be approved) |
Success response schema (matches authorize_purchase auto-approved success):
| Field | Type | Description |
|---|---|---|
authorized |
boolean | Always true |
transaction_id |
string | UUID of the Transaction row written by the claim (also the transaction_ledger_entry_id in completion_metadata) |
amount |
number | Echoed amount |
category |
string | Echoed category slug |
vendor |
string | null | Echoed vendor name |
envelope_remaining |
number | Envelope balance AFTER the debit |
pending_id |
string | Echoed for traceability |
Example success:
{
"authorized": true,
"transaction_id": "9e1d3a5c-2b4f-4d6e-8a9c-1b2c3d4e5f60",
"amount": 87.50,
"category": "groceries",
"vendor": "Whole Foods",
"envelope_remaining": 145.80,
"pending_id": "8b2c4d6e-3f1a-4d5b-9e7c-1a2b3c4d5e6f"
}
Error responses (mirror the HTTP status semantics from §3.11.7):
| MCP shape | HTTP status | Meaning |
|---|---|---|
{"status": "not_found"} |
404 | Cross-tenant, cross-agent, or unknown pending_id. |
{"status": "invalid_state", "current_status": "<x>", "reason": "pending_status_invalid", "message": "..."} |
409 | Row exists but status != approved (still pending, or already denied). The body carries the actual current status. |
{"status": "expired", "reason": "<reason>", "message": "..."} |
410 | Row was approved but the 15-minute window passed before the agent claimed, OR the agent token was deleted between approve and claim. The row has been flipped to expired by the call itself. |
{"error": "<detail>"} |
5xx / network | Transport-level error. NOT a policy decision; retryable. |
After a successful claim, the agent should proceed with the actual purchase on its own settlement rail (Stripe, card, etc.). Bank sync will reconcile the real merchant transaction against the Zado record later. The Zado record is the policy-and-evidence anchor; settlement is delegated.
4.7 Concurrency and atomicity
authorize_purchase uses a row-level FOR UPDATE lock on the agent token
row to prevent TOCTOU race conditions between guard evaluation and guard-state
update. On SQLite (which does not support FOR UPDATE), the underlying
connection-level write serialization provides the same guarantee. Concurrent
calls on the same token are serialized; concurrent calls on different tokens
for the same envelope race on the envelope row, with the loser receiving
envelope_empty if the balance flipped between check and commit.
complete_pending_authorization uses a different concurrency primitive: a
single SQL UPDATE ... WHERE status='approved' AND expires_at >= now
(see §3.11). The targeted UPDATE is itself the lock — under SQLite
(BEGIN EXCLUSIVE serializes writers) and PostgreSQL (row-level lock),
exactly one concurrent claim sees rowcount == 1 and proceeds to debit; all
losers see rowcount == 0 and re-read to produce the correct error response.
This eliminates the double-debit race that would otherwise be possible
between two agents (or two retries of the same agent) racing on the same
approved row.
5. Audit log
Every mutation initiated by an agent is automatically tagged with an audit
context capturing who acted, what they did, and when. Coverage is
automatic via AuditMiddleware — no per-route plumbing required, so future
endpoints inherit audit coverage without code changes.
5.1 Actor model
The audit system supports six actor types
(backend/app/core/audit_context.py):
| Actor type | Source |
|---|---|
user |
A human using the desktop UI with a session cookie |
assistant |
The Zado in-app AI coach |
rule |
An automated rule (categorization, recurring detection) |
sync |
Bank sync operations (Plaid / Teller) |
system |
Internal system operations |
mcp_agent |
An external AI agent via an MCP token |
For agent-initiated requests, actor_type is set to mcp_agent and
actor_details is populated with the agent's identity and current spend
posture (backend/app/api/deps.py populates the audit context as part of
get_user_context / get_agent_context).
5.2 Audit context schema
For an agent-initiated mutation:
| Field | Type | Description |
|---|---|---|
actor_type |
string | Always "mcp_agent" for agent calls |
actor_details.agent_id |
string (UUID) | The agent token's id |
actor_details.agent_name |
string | Human-readable agent name (e.g., "Claude Code", "OpenClaw") |
actor_details.scope |
string | "read" or "spend" |
actor_details.session_spend_so_far |
string (decimal) | Cumulative session spending at the moment of the action |
batch_id |
string (UUID) | Optional, groups related changes for bulk undo |
Example audit entry for an authorize_purchase success:
{
"id": "9b7f8c2d-1a4e-4f6b-83c1-d5e6f7a8b9c0",
"actor_type": "mcp_agent",
"actor_details": {
"agent_id": "1d73176f-5a24-4c8e-b21f-3a4b5c6d7e8f",
"agent_name": "OpenClaw",
"scope": "spend",
"session_spend_so_far": "47.50"
},
"action": "transaction.create",
"entity_type": "transaction",
"entity_id": "8f2a1e3c-7b40-4d8e-9c71-c1d2e3f4a5b6",
"before": null,
"after": {
"amount": 43.20,
"category_slug": "groceries",
"vendor": "Whole Foods",
"agent_token_id": "1d73176f-5a24-4c8e-b21f-3a4b5c6d7e8f"
},
"occurred_at": "2026-04-25T18:42:11.382Z"
}
5.3 Coverage and exportability
- Coverage: All POST/PUT/PATCH/DELETE requests are audited via
backend/app/middleware/audit.py. Read-only requests are NOT audited (they do not mutate state). - Exportability: The audit log is exportable by the user. The export
shape is a stable JSON list of audit entries; the export endpoint is part
of the data-export surface (
backend/app/api/export/).
6. Security model
This section is normative. v0.1 covered defense-in-depth in passing; v0.2 elevates security to the gating concern it actually is for agentic finance adoption. The question every partner asks first — "what's the worst that can happen if my Zado-issued agent token is compromised?" — is answered directly here, in three subsections: threat model, blast radius, and reversibility / audit.
6.1 Threat model
The threats v0.2 explicitly defends against:
- Token theft. An attacker obtains an agent's Bearer token (e.g., reads it from a misconfigured MCP host config file, exfiltrates it from a compromised dev machine). Mitigation: tokens are SHA-256 hashed at rest and shown plaintext only once at registration; revocation is one click via Settings → Agent Access; per-transaction cap, session cap, envelope binding, and pace multiplier bound the attacker's spend before kill-switch.
- Confused deputy. A legitimate agent is coerced (by prompt injection,
user mistake, or compromised model output) into spending in a category
outside its intended scope, or above its intended cap. Mitigation:
envelope binding (per-token
allowed_category_ids) + per-transaction cap- HAG threshold. The agent's binding is set by the human at registration; no agent-side request can widen it.
- Cross-tenant leak. An attacker with one user's agent token attempts
to read or spend against another user's envelopes. Mitigation: every
server-side query carries a
user_idfilter; agent tokens carry auser_idFK that the auth layer pins on every request. There is no API surface that returns data withoutuser_idscoping. - Prompt injection coercing higher-cap spend. An attacker poisons agent
context to make it ask Zado for an above-policy spend. Mitigation: HAG.
When the token's
requires_human_approval_thresholdis set, every injection-induced large spend parks for human review; the human is the final gate on out-of-pattern requests. - Replay against idempotency. An attacker re-submits a captured
request (e.g., a stolen
complete_pending_authorizationcall) hoping for a second debit. Mitigation:complete_pending_authorizationis idempotent onpending_id(CAS-protected, returns cachedcompletion_metadataon replay). The user-side approve/deny endpoints honor anIdempotency-Keyheader so duplicate clicks return the cached response. - Expired-pending claim. An attacker holds an old
pending_idand attempts to claim it long after the user thought it was resolved. Mitigation: every read inline-expires stale rows (covers bothpendingpast TTL ANDapprovedpast TTL), and the CAS on claim re-checksexpires_at >= now; an expired row returns410and is permanently unclaimable.
The threats v0.2 does NOT defend against (left to the user's broader security posture):
- Compromised user account (session-cookie theft). The human session is the root of trust; if the user's primary auth is compromised, an attacker gets the same surface the user has, including the ability to register new agents.
- Compromised Zado backend. The spec assumes the policy-evaluation server is operating correctly. Multi-region byzantine fault tolerance is out of scope.
- Side-channel inference (an agent learning user habits from observed envelope balances over time). Within scope is what the agent can do, not what it can infer.
6.2 Blast radius
What an agent literally cannot do, even with a stolen token, even with prompt-injected goals, even with full local-runtime compromise:
- Cannot spend outside bound categories. If the token has
allowed_category_ids, everyauthorize_purchaseagainst an unbound category returnsenvelope_not_bound. Server-side check; the local MCP process cannot bypass it. Read-side endpoints are similarly bound — a bound agent readinglist_envelopessees only its bound envelopes (the envelope-binding read-path filter is centralized so single-envelope and aggregation endpoints share the same scoping logic). - Cannot exceed the per-transaction cap. Configurable per token
(default
$50); a single request above this returnsper_transaction_cap_exceeded. Splitting attempts into many smaller requests hits the next limit: - Cannot exceed the session cap. Cumulative across all of an agent's
purchases since the last 24-hour idle reset. Default
$100; configurable per token. Hitssession_cap_exceededand the agent must wait the reset window or have the user raise the cap. - Cannot bypass the rate limit. Maximum 3
authorize_purchasecalls per minute per agent. Hitsrate_limitedwith aretry_after_seconds. - Cannot drain an envelope faster than the pacing guard allows. The
pace guard rejects purchases consuming more than
pace_multiplier × daily_pace. An agent trying to burn the rest of the envelope in one request is throttled. - Cannot hit envelopes outside the binding allowlist even by
category-name spoofing: the binding check resolves the request's
category to a stable category UUID before comparing against
allowed_category_ids. - Cannot see other users' data. Every read filters by
user_id. There is no API surface that returns cross-user data; cross-tenant probes return404(uniform with not-found) so existence is not leakable. - Cannot persist beyond TTL. Tokens carry a mandatory
expires_at(default 90 days, configurable 1–90). After expiry, the token returns401on any call. - Cannot bypass HAG when configured. With
requires_human_approval_thresholdset, every spend at or above the threshold parks for human approval — and even after approval, the agent must explicitly claim viacomplete_pending_authorizationto debit. There is no agent-side path to convert an above-threshold request into an immediate debit.
The architectural guarantee underneath all of the above is tenant
isolation in the data layer: user_id scoping on every query, agent token
binding to user_id, envelope-binding read-path filtering applied via a
centralized helper across all envelope-aware endpoints, per-user partitioning
of graph memory and receipt archive. These guarantees have been
adversarially audited multiple times in 2026-04 (independent Codex and
Gemini paranoid-audit sweeps) — every flagged finding from those sweeps was
either fixed or formally rejected with reasoning, and the deletion registry
covers every user-scoped model so account deletion provably removes all
user-bound rows.
6.3 Reversibility and audit
Every state transition that involves a Zado-mediated agent spend is recorded, surfaced to the user, and reversible up to the point of external settlement. This is the third leg of the trust posture — defense-in-depth contains the blast; reversibility makes the rest recoverable.
- Audit log on every transition. Every agent-initiated mutation —
authorize-success, defer-to-pending, user approve, user deny, expire,
agent claim — writes an
AuditLogrow capturing actor (mcp_agentoruser), action, before/after state, and timestamp. Capture is automatic via the audit middleware; future endpoints inherit coverage without per-route plumbing. (See §5.) completion_metadataties human "yes" to ledger debit. The per-pending-rowcompletion_metadataJSON (§3.11.5) records thetransaction_ledger_entry_id,envelope_id_at_debit,debited_amount,completed_at, andenvelope_remaining_at_debitof the claim. A third party reconciling an agent spend can match the human's approval (in audit log) → the agent's claim (in audit log) → the actual ledger entry (viatransaction_ledger_entry_id) without ambiguity, while also preserving the post-debit envelope balance the caller observed.- Activity feed surfaces all agent attempts, including denials. The desktop app's Agent Activity feed shows every authorize-success, authorize-rejection (with reason code), defer, and completion. A user can see "your agent tried to spend $300 on dining and was blocked by the pacing guard" without opening logs.
- Transactions are disputable and reversible. Any agent-initiated transaction enters the same dispute and rollback surfaces a human-entered transaction does. The ledger is reversible; settlement is the only irreversible step (and Zado does not perform settlement — that is delegated to the agent's chosen rail).
- Kill switch. Settings → Agent Access → Freeze All Agents revokes
every active agent token in one action; revoked tokens return
401on the next call and any in-flight pending rows the agent owns can no longer be claimed.
6.4 Defense in depth
A successful authorize_purchase (auto-approved path) traverses eight
distinct enforcement layers in v0.2, any one of which can deny:
| # | Layer | Enforcement surface |
|---|---|---|
| 1 | Token hashing (SHA-256 at rest) | Token storage |
| 2 | Bearer token transport over HTTPS | MCP → Backend |
| 3 | Token lookup + is_active + TTL check (revoked or expired → 401) |
Backend dependency injection |
| 4 | Scope enforcement (spend required for authorize_purchase) |
agent_spending.authorize_purchase + AgentScopeMiddleware allowlist |
| 5 | Envelope binding check (category must resolve to one in allowed_category_ids) |
agent_guard.check_envelope_binding |
| 6 | Spending guards (per-tx cap, session cap, rate limit, pacing) | agent_guard.check_and_record_spend |
| 7 | Envelope balance check (envelope remaining >= amount) |
SpendingService.check_can_afford |
| 8 | Human Approval Gate (defer if amount >= threshold) |
agent_spending.authorize_purchase step 5 (v0.2) |
A request that passes layer 4 but fails layer 5 produces envelope_not_bound.
A request that passes layer 6 but fails the envelope balance at layer 7
produces envelope_empty and the guard-state mutation is rolled back so the
rejected attempt does not consume the agent's session cap. A request that
passes all balance checks but meets the HAG threshold at layer 8 defers
(see §3.11) — the envelope is not debited until the agent's later claim. A
claim itself is gated by a CAS UPDATE that requires the row to be approved
AND not yet expired (see §4.7); concurrent claims serialize to exactly one
debit.
What agents CAN see (recap from §3 + above): envelope names and balances
for bound envelopes; daily allowance, days-remaining, alerts; affordability
check results; for spend scope, the transaction record they themselves
created.
What agents CANNOT see (architectural, not policy): bank account/routing
numbers; bank connection tokens (Plaid/Teller access_token — these are
encrypted at rest and never returned in any API response, audit entry, or
coach context); raw transaction history outside bound categories;
user PII (email, phone, address) beyond what envelope names happen to expose;
other agents' tokens or activity; any other user's data. The architectural
guarantee: the local MCP process has zero direct database access — it
can only call the documented HTTPS endpoints with its Bearer token, and no
endpoint returns the data above (it isn't in any response shape the agent
allowlist permits).
7. Integration examples
This section is informative. Three patterns cover the common integration shapes. All examples assume the integrator has already registered an agent in the Zado app (Settings → Agent Access → Create Agent) and obtained a Bearer token.
7.1 Install — pip install zado-mcp
The canonical install for v0.2 is the packaged
zado-mcp PyPI distribution. One-line
install:
pip install zado-mcp
Requires Python ≥ 3.10. Two transitive dependencies (mcp, httpx).
Interactive setup. After install, run:
zado-mcp init
The interactive flow prompts for the Zado instance URL (default
https://zadofi.ai) and the agent token (input is hidden), then writes
~/.config/zado-mcp/config.json with mode 0600. The token never appears in
the printed .mcp.json snippet — it lives only in the 0600 config file.
Canonical MCP host config. Paste this snippet into your agent's MCP
config (Claude Code .mcp.json, Claude Desktop's
claude_desktop_config.json, OpenAI Agents SDK's MCP server list, etc.):
{
"mcpServers": {
"zado": {
"command": "zado-mcp",
"args": ["serve"]
}
}
}
No environment block is required for the typical case — zado-mcp serve
reads URL + token from the config file written by init. Override per-host
(e.g., point at a local backend during dev) by setting ZADO_API_URL and
ZADO_AGENT_TOKEN in the host's env block; env vars take precedence over
the config file.
Restart the host. The six tools (check_budget, list_envelopes,
get_daily_status, authorize_purchase, check_pending_authorization,
complete_pending_authorization) appear in the available tool set under
the zado namespace.
7.1.1 Developer / local-checkout alternative
For contributors working on the Zado backend directly (not the typical
integration path), the historical
python -m app.mcp.server invocation is still supported as a thin shim that
re-exports from the zado_mcp package. Use this only when you need to run
the MCP server out of a local checkout against an unreleased backend
revision; otherwise prefer the packaged install above.
{
"mcpServers": {
"zado-dev": {
"command": "python",
"args": ["-m", "app.mcp.server"],
"cwd": "/path/to/zado/backend",
"env": {
"ZADO_API_URL": "http://localhost:8001",
"ZADO_AGENT_TOKEN": "zado_agent_..."
}
}
}
}
This form is not the canonical install path for v0.2 integrators.
7.2 Generic MCP stdio client
For non-Claude agent runtimes, any MCP-compatible client can call Zado
tools over stdio. Using the canonical zado-mcp install:
import asyncio, os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
# Token + URL come from ~/.config/zado-mcp/config.json (written by
# `zado-mcp init`). Override with env vars if needed.
params = StdioServerParameters(command="zado-mcp", args=["serve"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
budget = await session.call_tool(
"check_budget", {"category": "groceries"}
)
print(budget)
# Phase 1: authorize. Returns a deferral if the amount meets
# the per-token requires_human_approval_threshold.
result = await session.call_tool(
"authorize_purchase",
{"amount": 43.20, "category": "groceries", "vendor": "Whole Foods"},
)
if result.get("reason") == "pending_human_approval":
pending_id = result["pending_id"]
# Phase 2: poll, then claim.
while True:
status = await session.call_tool(
"check_pending_authorization", {"pending_id": pending_id}
)
if status["status"] == "approved":
break
if status["status"] in ("denied", "expired", "not_found"):
return # human said no, or window passed
await asyncio.sleep(2)
claim = await session.call_tool(
"complete_pending_authorization", {"pending_id": pending_id}
)
print(claim) # authorized=True with transaction_id
else:
print(result) # auto-approved or rejected
asyncio.run(main())
The same pattern works in any language with an MCP client library (TypeScript SDK, Go SDK, etc.). The wire protocol is identical; only the SDK surface differs.
7.3 OpenClaw end-to-end (real-world reference)
This is the integration that produced the 2026-04-03 first-loop. The flow:
- Agent receives a shopping intent from the user via OpenClaw's normal intake (e.g., "buy groceries at Whole Foods, around $45").
- Agent calls
check_budget("groceries")to confirm the envelope has capacity. Response:{ "remaining": 47.50, "budgeted": 400.00, "spent": 352.50, ... }. - Agent reads the response and decides the purchase fits.
- Agent calls
authorize_purchase(43.20, "groceries", "Whole Foods"). The server runs scope check → binding check → guards (caps, rate limit, pacing) → envelope balance check → atomic commit. Response:{ "authorized": true, "transaction_id": "...", "envelope_remaining": 4.30 }. - The transaction appears immediately in the user's ledger with
actor_type: "mcp_agent"andactor_details.agent_name: "OpenClaw"in the audit log. - The user can revoke the agent at any time via Settings → Agent Access → Freeze All Agents, instantly invalidating the token.
Settlement note: in v0.2 (as in v0.1), authorize_purchase (auto-approved)
and complete_pending_authorization (claim) record a Zado ledger entry
representing a policy decision that the spend is permitted. They do not
themselves move money on a card rail. The v0.3+ roadmap (§9) describes how a
Zado authorization can bind to a Stripe Issuing one-time virtual card so the
authorization and the card-rail charge become a single closed-loop
transaction.
Two-phase HAG variant (when the agent token has
requires_human_approval_threshold set and the amount meets it): step 4
returns reason: "pending_human_approval" with a pending_id; the user
sees the request in the Pending Authorizations inbox; agent then calls
check_pending_authorization(pending_id) until status is approved, then
complete_pending_authorization(pending_id) to atomically claim the
approval. See §3.11 for the full lifecycle.
8. Compatibility with adjacent protocols and platforms
This section is informative. It describes how Zado v0.2 maps to (or deliberately does not map to) the agent-payment work being done by other players. Zado is designed to complement, not compete with, each of these.
8.1 Google AP2 — Agent Payments Protocol (v0.2 vocabulary)
Zado fits the AP2 v0.2 Trusted Surface role in the Trusted Agent Provider model — formally defined in AP2's Agent Authorization Framework as the secure, non-agentic interface that renders Mandate Content to the user for authorization, producing mandates as output. Zado satisfies the three normative properties of a Trusted Surface implementation: non-agentic (HAG UI is human-controlled, not LLM-driven), user-consent gate (no mandate produced without explicit approval), separation of duties (agent cannot bypass HAG even with a stolen token). Zado emits LOCAL artifacts today (no JWS, no SD-JWT VC, no Verifier-signed JWT); the v0.3 roadmap (§9) targets emission of AP2-compatible portable evidence.
Mapping table (Zado v0.2 ↔ AP2 v0.2):
| AP2 v0.2 primitive | Zado v0.2 local equivalent | Portable emission status |
|---|---|---|
| Trusted Surface (role) | Zado IS this role — desktop app + HAG approval UI fulfill the Trusted Agent Provider model | Role assignment, not a credential — no emission needed |
Open Mandate (constraints + cnf key) |
agent_token (scope, allowed_categories, per_transaction_cap, session_spending_cap, pace_multiplier, requires_human_approval_threshold), bound to user_id |
Local row today; v0.3 roadmap targets emission as SD-JWT VC with cnf claim |
| Closed Mandate (Open + transaction binding via Key Binding JWT) | pending_authorization row with status ∈ {pending, approved}, bound to specific amount + vendor + category |
Local row today; v0.3 roadmap targets emission as Open Mandate + Key Binding JWT pair |
Mandate Receipt (Verifier-signed JWT: iss, result, reference, error?, error_description?) |
completion_metadata JSON: {transaction_ledger_entry_id, envelope_id_at_debit, debited_amount, completed_at, envelope_remaining_at_debit} written at complete_pending_authorization |
Local JSON today; v0.3 roadmap targets emission as Verifier-signed JWT per AP2 v0.2 Mandate Receipt schema |
| Action Authorization (Verifier challenges Agent for proof) | complete_pending_authorization MCP tool — agent must explicitly claim approved authorization; CAS atomic update prevents double-claim |
Conceptually equivalent today; v0.3 upgrades to cryptographic proof via Open→Closed Mandate binding |
| Checkout Mandate | Not implemented — Zado does not interact with merchant carts | v0.4+ (requires merchant integration layer; out of scope for v0.2 + v0.3) |
| Payment Mandate | Not implemented — Zado does not bind to settlement rails (no card issuance) | v0.4+ (requires Stripe Issuing or equivalent; out of scope for v0.2 + v0.3) |
Zado's HAG flow corresponds to AP2 v0.2's
Human-Not-Present → Human-Present recovery
pattern. When requires_human_approval_threshold is exceeded, an
authorize_purchase request that would otherwise auto-approve is parked as
a pending_authorization and the user is invited back in — precisely AP2's
unresolved_constraint error → Human-Present upgrade path. AP2 v0.2 was
donated to the FIDO Alliance (announced 2026-05 era) for standardization in
the Agentic Authentication Technical Working Group and Payments Technical
Working Group; Zado's role-fit positioning above tracks the FIDO-stewarded
direction.
8.2 Stripe Machine Payments Protocol (MPP)
Stripe MPP (launched March 18, 2026) defines scope-bound, time-bound credentials for agents to invoke Stripe-rails payments. MPP focuses on the execution side of agent payments: how an agent presents itself to Stripe, what scope of payment it can initiate, and how Stripe authorizes the rail call.
Zado mapping: Zado is the consumer-side policy layer an MPP-scoped agent can consult before invoking MPP-routed payment. The flow:
- Agent receives intent.
- Agent calls Zado
check_budgetand/orauthorize_purchaseto confirm the spend is permitted by the user's envelope policy. - If Zado authorizes, the agent invokes its MPP credential to actually move money on Stripe rails.
- The Zado authorization + Stripe MPP charge are two halves of a closed-loop transaction (today, the linking is done by the integrator; v0.3+ roadmap binds a Stripe Issuing virtual card to a Zado authorization so the link is automatic).
Zado does not duplicate or replace any Stripe MPP capability. They sit at different layers.
8.3 Visa Intelligent Commerce Connect
Visa Intelligent Commerce Connect (April 2026, GA expected June 2026) provides network-side payment infrastructure for AI agents — tokenization, spend controls, multi-network acceptance. Like MPP, it focuses on the execution side, not consumer-side policy.
Zado mapping: Zado is the consumer-side budget gate that complements
network-side spend controls. A Visa-Intelligent-Commerce-routed agent
purchase can consult a Zado-equivalent policy layer for envelope-level
authorization before the network sees the request. v0.3+ roadmap: Zado
publishes a network-callable HTTPS endpoint (POST /api/agents/network/preauth)
that returns a normalized permitted: true|false response keyed by Visa
agent ID for direct integration with network-side tokenization flows.
8.4 Mastercard Agent Pay
Mastercard Agent Pay (rolling out to all US Mastercard cardholders by Q4 2026 holiday season) provides Agentic Tokens that consumers can configure with per-agent spend scopes. Like Visa IC, this is network-side and consumer-controlled — but the consumer interface is provided by Mastercard's own platform.
Zado mapping: Zado offers an alternative consumer surface for the same job — bank-account-backed (via Plaid) rather than card-network-bound, with envelope semantics that fit how ADHD-impacted users actually budget. The two are not mutually exclusive: a consumer can have both a Mastercard Agentic Token (for card-rail purchases) AND a Zado token (for any ACH-funded purchase or any agent-runtime that uses MCP rather than a card network's SDK).
8.5 Model Context Protocol (MCP)
Zado tools are exposed via standard MCP via the
zado-mcp PyPI package. The
protocol-level conformance:
- All six tools registered via
FastMCP.tool()decorators inzado_mcp/tools.py. - Stdio transport (local-only in v0.2).
- No bespoke extensions to the MCP protocol — any MCP-compatible client works.
v0.3+ roadmap: HTTP+OAuth2.1 transport per the MCP Authorization spec — see §9. Required for any partner needing SaaS or multi-tenant deployment; the enterprise-readiness gate.
9. Future capabilities (v0.3+ roadmap)
The following capabilities are explicitly NOT shipped in v0.2. Integrators MUST NOT depend on them today. They are listed here so partners can plan and so the spec's evolution is transparent. (HAG, the headline v0.1 → v0.2 roadmap item, is now normative — see §3.11.)
JWS / signed authorization-proof emission from
complete_pending_authorization. Today, the claim returns a JSONcompletion_metadatablock (§3.11.5) — a local artifact, not a portable credential. v0.3 targets a signed proof (JWS) so a third party verifying a Zado-mediated agent spend can do so without trusting the Zado backend at the moment of verification. NOT SHIPPED — emission targeted for v0.3. Sketch of the JWS payload:{ "sub": "<agent_token_id>", "iss": "<zado_instance_url>", "iat": "<completed_at>", "exp": "<completed_at + N seconds>", "claim": { "transaction_ledger_entry_id": "<uuid>", "envelope_id_at_debit": "<uuid>", "debited_amount": "<decimal-string>", "completed_at": "<ISO-8601 UTC>", "threshold_at_authorization": "<decimal-string>", "envelope_remaining_after_debit": "<decimal-string>" } }Signed with a per-Zado-instance signing key whose JWKS is published at a well-known URL. Maps to AP2 v0.2's Mandate Receipt schema (see §8.1).
AP2 VDC emission for Open Mandate and Closed Mandate. Token issuance produces a signed AP2 v0.2 Open Mandate (SD-JWT VC with
cnfclaim);complete_pending_authorizationsuccess produces a signed AP2 v0.2 Mandate Receipt JWT. Lets Zado-authorized spend flow into AP2 ecosystems without external signing.Stripe Issuing one-time virtual card binding (HAG Phase 6). A successful claim issues a single-use virtual card via Stripe Issuing bound to the authorized amount, vendor (where supported), and a short expiry. The card-rail charge and the Zado claim become a closed loop that reconciles automatically via bank sync.
SMS channel for HAG approval (HAG Phase 3). A non-app-resident channel for approve/deny — text message to the user's verified phone number with one-tap response. Gated on Phase 2 voice-validation tester data (currently in field).
Graph-memory rebalance suggestions (HAG Phase 5). When a deferred authorization arrives and the bound envelope is empty but adjacent envelopes have unspent budget, the HAG approval UI suggests a one-tap rebalance: "Move $40 from Dining to Groceries to approve?" Gated on user-cohort data showing the suggestion is actually predictive.
Refund and dispute reconciliation. When a merchant refunds an agent-initiated purchase, the corresponding envelope balance auto-corrects and the agent's
session_spending_totalcredits back. Dispute lifecycle events update the audit trail.HTTP+OAuth2.1 MCP transport — enterprise-readiness gate. Per the MCP Authorization spec, HTTP transport with OAuth2.1 is the standard authorization profile for multi-tenant deployments.
zado-mcpv0.2 ships stdio-only; HTTP+OAuth is the obvious next priority for any partner needing SaaS deployment. This is the single largest blocker between Zado and a hosted multi-tenant product story; it is called out separately because every partner conversation lands on it.Multi-user envelope approval. Couples and small businesses where envelope spending requires N-of-M human approvers. Same envelope-as- permission model, with an additional approval-quorum primitive on the envelope.
Network-side preauth endpoint.
POST /api/agents/network/preauthreturnspermitted: true|falsewith envelope context for direct consumption by Visa Intelligent Commerce or Mastercard Agent Pay network-side flows.
The version that ships any of these capabilities will increment to v0.3 or later per the versioning policy in Section 10.
10. License and versioning
10.1 License
This specification is licensed under CC-BY 4.0. You may share, adapt, and build upon it for any purpose, including commercial use, with attribution to Evolving Intelligence AI, LLC and a link to this canonical URL.
The reference implementation in this repository is licensed under the
project's existing license (see project root LICENSE).
10.2 Versioning policy
The spec follows a relaxed semantic versioning model:
- Patch versions (0.1.x): editorial corrections, clarifications, fixes to ambiguous wording. No behavioral changes. No deprecations.
- Minor versions (0.x): MAY add new tools, fields, rejection codes, or scopes. MUST NOT remove or rename existing tools, fields, rejection codes, or scopes. MUST NOT change the meaning of an existing field.
- Major versions (1.0+): MAY break compatibility. A migration guide MUST accompany any major version.
An integrator targeting v0.1 can rely on every documented surface continuing to work in any v0.x release.
10.3 Citation
Cite this spec as:
Zado Agent Trust Protocol v0.2, Evolving Intelligence AI, 2026-05-03. https://zadofi.ai/protocol/zado-agent-trust-protocol-v0.2
BibTeX is in docs/protocol/README.md. The v0.1 BibTeX entry is preserved alongside v0.2's.
Appendix A — Source-of-truth traceability
This appendix is informative. It lists the publicly-distributed source locations that back the normative claims in this spec. Citations use file + symbol form (not line numbers) — symbols are stable across reformats; line numbers go stale within weeks of publication.
Scope. This appendix is limited to publicly-distributed code that any reader can verify without access permissions:
- The packaged
zado-mcpPyPI distribution. Anyone canpip download zado-mcpand read the wheel contents. - This specification document itself.
The Zado backend repository (evo-hydra/zado) is currently private; backend
internals are intentionally not cited inline to avoid leaking
private-repository architecture. Section §6 describes the backend security
guarantees in prose without making private paths navigable. If/when the
backend is open-sourced, a v0.3 spec revision will expand this appendix to
include the relevant backend file + symbol citations.
| Spec section | Normative claim | Source (publicly verifiable) |
|---|---|---|
| §3.11 / §4.1 | check_budget MCP tool — wire shape, request/response |
tools/zado-mcp/zado_mcp/tools.py::check_budget |
| §4.2 | get_daily_status MCP tool — wire shape, alerts schema |
tools/zado-mcp/zado_mcp/tools.py::get_daily_status |
§4.3 (and §4.2 in list_envelopes table) |
list_envelopes MCP tool — wire shape, per-envelope detail |
tools/zado-mcp/zado_mcp/tools.py::list_envelopes |
| §4.4 / §3.11 | authorize_purchase MCP tool — wire shape, deferral hint |
tools/zado-mcp/zado_mcp/tools.py::authorize_purchase |
| §3.11 / §4.5 | check_pending_authorization MCP tool — wire shape, status enum projection |
tools/zado-mcp/zado_mcp/tools.py::check_pending_authorization |
| §3.11.7 / §4.6 | complete_pending_authorization MCP tool — 404/409/410 mapping |
tools/zado-mcp/zado_mcp/tools.py::complete_pending_authorization |
| §7.1 | Canonical CLI: zado-mcp init, zado-mcp serve |
tools/zado-mcp/zado_mcp/cli.py::main |
| §7.1 | Default API URL + config-file resolution order | tools/zado-mcp/zado_mcp/cli.py::DEFAULT_API_URL |
| §6 | The local MCP process has zero direct database access; all enforcement is server-side | tools/zado-mcp/zado_mcp/tools.py (every tool delegates to ZadoAPIClient; no DB imports) |
| §1, §3 | Spec catalog README + version index | docs/protocol/README.md |
A normative claim that does not appear in this appendix is still backed by shipped code — it is simply backed by code in the private backend repository and described in spec prose rather than navigable citation.
Specification version: 0.2
Date: 2026-05-03
Supersedes: v0.1 (2026-04-25) — preserved unchanged at
docs/protocol/zado-agent-trust-protocol-v0.1.md
Reference implementation: zado-mcp >= 0.2.0, < 0.3.0 (PyPI)
Author: Evolving Intelligence AI, LLC
Spec license: CC-BY 4.0
Reference implementation license: see project LICENSE
Canonical URL: https://zadofi.ai/protocol/zado-agent-trust-protocol-v0.2
Source repository: see project README.md
Publicly-distributed reference-implementation source:
tools/zado-mcp/zado_mcp/tools.py,
tools/zado-mcp/zado_mcp/cli.py,
tools/zado-mcp/zado_mcp/client.py,
tools/zado-mcp/zado_mcp/server.py,
tools/zado-mcp/README.md.