architecture · data and trust
What Ava stores, and why you can check it
Ava’s claim is narrow and testable: an agent moved capital only inside terms a user agreed to, and anyone can check what settled against what was authorised. This page shows the data path that makes that claim, the credentials that decide who may spend, where every record lives, and where the system does not yet meet its own bar.
Data flow
Information enters as words, structured arguments and a credential. It is validated before anything reads it as intent, decided by deterministic gates, acted on only after a fresh read of chain state, and recorded in a form a third party can recompute.
Rendering diagram
flowchart TB
subgraph enter["1 · Enters"]
direction LR
nl["Natural language<br/>mandate · workflow · copilot"]
structured["Structured args<br/>REST body · MCP arguments"]
cred["Credential<br/>Bearer · API key"]
nl ~~~ structured ~~~ cred
end
subgraph validate["2 · Validated"]
direction LR
zod["zod schemas"]
principal["Principal from credential<br/>userId args compared, never trusted"]
parse["Deterministic parsers<br/>model only as optional fallback"]
zod ~~~ principal ~~~ parse
end
subgraph decide["3 · Decided"]
direction LR
reg["Capability registry"] --> gates["Mandate and policy gates"] --> reserve["Claim + budget reservation"]
end
subgraph act["4 · Acted on"]
direction LR
preview["Preview from chain state"] --> sign["Turnkey signature"] --> chain["Broadcast + finality"]
end
subgraph record["5 · Recorded"]
direction LR
attest["Attestation<br/>tx re-read and compared"] --> receipt["Receipt<br/>hashed · chained · signed"] -.-> anchor["Anchor on Base<br/>out of band"]
end
enter --> validate --> decide --> act --> record
record --> out(["Verified by hash: agent · user · third party"])Each stage can stop the flow, and every stop returns a typed refusal with filled false. Only the last stage produces something a caller may report as done.
Where language models sit
Natural language is parsed deterministically first: parseMandateMessage for mandates, decomposeWorkflow for workflows, a heuristic parser for the copilot. Only the copilot can fall back to a model (OpenRouter, behind a 2.5 second deadline and only for trade-shaped messages), and its output is an AvaIntent that then meets every gate like any other input. No model decides whether to sign.
Identity and credentials
Identity comes from exactly one place: the principal the auth middleware derives from a presented credential. Routes and tools never read identity from a header, query string or body, and a presented credential that does not resolve is a 401 rather than a silent downgrade to anonymous. Source: middleware/auth.ts, lib/principal.ts, lib/auth-tokens.ts.
| presented as | kind | audience | notes |
|---|---|---|---|
| ava_st_… | Session token | spend | Minted by POST /v1/users/session or ava_session. Shown once; only its SHA-256 digest is stored. |
| ava_dt_… | Dashboard token | view | Minted only by exchanging a device code. Authenticates, cannot reach any live-spend route or tool. |
| ava_dc_… | Device code | exchange | Ten-minute, single-use. Travels in a URL fragment so it is never logged by a server. |
| API key | AVA_API_KEYS | spend | Bound to exactly one userId at boot. There is no key that can act as an arbitrary tenant. |
| x-ava-agent-credential | Scoped agent credential | per scope | Issued by /v1/principals/verify after the user's wallet signs an EIP-712 challenge. Checked by the live execution gate on Compound and swap legs. |
| x-ava-operator-token | Operator token | operator | Pause-all, resume-all, per-execution pause and revoke, usage. Routes are disabled when AVA_OPERATOR_TOKEN is unset. |
| x-ava-user-id | Assertion | none | A claim, compared with the credential and refused on mismatch. Trusted as identity only on a developer machine with auth explicitly opened. |
Ownership answers 404
Asking for someone else’s mandate, receipt or owner-only record is answered exactly as if the id did not exist. A 403 would confirm that the id is real and let ids be enumerated.
Wallet custody
Ava-provisioned wallets are custodial: Turnkey holds the key in a sub-organization created for that user, and the API signs with it after every gate passes. The signer refuses a transaction whose from is not its own address and checks that what Turnkey returns still carries the destination and calldata it was given. Users can instead link an external wallet with POST /v1/users/me/wallets/link; Ava then observes and records but never signs for it.
Storage
In production the ledger mode is postgres and every store on the path to a signature is durable, because a reservation or revocation that evaporates on deploy is a promise the product cannot keep. Schemas live in packages/ledger-postgres/src and are applied idempotently at boot.
Rendering diagram
erDiagram
ava_tenancy_users ||--o{ ava_tenancy_agent_instances : owns
ava_tenancy_users ||--o{ ava_tenancy_wallets : owns
ava_tenancy_users ||--o{ ava_auth_tokens : "holds digests of"
ava_tenancy_users ||--o{ ava_mandates : authorises
ava_tenancy_agent_instances |o--o{ ava_mandates : "may hold"
ava_mandates ||--o{ ava_receipts : "bound by mandateHash"
ava_receipts ||--o{ ava_receipt_chain : "sequenced in"
ava_receipt_chain }o--o| ava_receipt_anchors : "covered by"
ava_execution_claims ||--o| ava_policy_reservations : "budget bound to"
ava_tenancy_users ||--o{ ava_standing_envelopes : signs
ava_standing_envelopes ||--o| ava_standing_revocations : "ended by"
ava_tenancy_users ||--o{ ava_agent_credentials : "is granted"
ava_tenancy_users {
text user_id PK
text display_name
}
ava_tenancy_wallets {
text wallet_id PK
text user_id FK
text family "evm, solana, sui"
text address
text status "pending_provision, active"
}
ava_auth_tokens {
text token_hash PK "SHA-256, never the token"
text token_id
text audience "spend or dashboard"
timestamptz revoked_at
}
ava_mandates {
text mandate_id PK
text terms_hash
text signer_address "EIP-712, optional"
jsonb mandate
}
ava_receipt_chain {
text receipt_hash PK
text chain_id
bigint sequence
text prev_receipt_hash
text signature_status
}
ava_execution_claims {
text claim_key PK
text state
text intent_hash
numeric reserved_amount_base
}
ava_standing_envelopes {
text principal_id PK
text envelope_id PK
text envelope_hash
text signer
integer moves_today
}A selection of the 26 tables, with the columns that carry the trust argument. Mandates are stored whole as JSON beside an indexed terms hash.
Durable and not
| data | lives in | where |
|---|---|---|
| Users, agents, wallets | Postgres | ava_tenancy_* |
| Session and dashboard tokens, device codes | Postgres | ava_auth_tokens, ava_device_codes |
| Mandates and refusals | Postgres, with an in-memory cache hydrated at boot | ava_mandates, ava_mandate_refusals |
| Receipts and their chain | Postgres | ava_receipts, ava_receipt_chain |
| Receipt and record visibility | Postgres | ava_receipt_visibility, ava_record_visibility |
| Anchors | Postgres | ava_receipt_anchors, ava_receipt_anchor_heads |
| Execution claims, nonce leases, budgets | Postgres | ava_execution_claims, _nonce_leases, _budgets |
| Daily notional and exposure reservations | Postgres | ava_policy_reservations |
| Standing envelopes and revocations | Postgres | ava_standing_envelopes, ava_standing_revocations |
| Principal challenges, proofs, agent credentials | Postgres | ava_principal_*, ava_agent_credentials |
| Workflow ledger | Postgres | ava_executions, ava_domain_events, ava_outbox, ava_idempotency |
| HTTP Idempotency-Key replay cache | Process memory | Map in createApp |
| Automations | Process memory | InMemoryAutomationStore, even in Postgres mode |
| Copilot pending plans, previews, paper balances | Process memory | lost on restart |
| Policy state read by the policy gate | Process memory | the durable commit is the reservation table |
The receipt model
Every receipt reaches storage through one wrapper, lib/receipt-chain.ts, which hashes, chains and optionally signs it. A receipt chains under the agent instance that holds its mandate, or under its user when no agent is named, so a gap in an agent’s history is visible.
Rendering diagram
flowchart LR r1["Receipt n-1<br/>receiptHash h1"] --> r2["Receipt n<br/>prevReceiptHash = h1<br/>receiptHash h2"] r2 --> r3["Receipt n+1<br/>prevReceiptHash = h2"] r2 -. "Ed25519 over hex(h2)<br/>when a key is configured" .-> sig["integrity.signature"] r2 -. "head published on Base" .-> anc["Anchor tx<br/>chainId · sequence · receiptHash"]
| field | how it is computed |
|---|---|
| receiptHash | sha256 of the RFC 8785 canonical JSON of the receipt, excluding its own hash and signatures. |
| prevReceiptHash | The receiptHash of the previous receipt in the same chain id. |
| integrity.signature | Ed25519 over the lowercase hex of receiptHash, by AVA_RECEIPT_SIGNING_KEY. Absent key: an unsigned reason is recorded instead. |
| mandateHash | sha256 of the canonical mandate terms: id, user, portal, objective, capital, constraints, createdAt. |
| gate.digest | sha256 of the canonical gate pre-image, so the decision to sign is replayable. |
| anchor | A Base transaction whose calldata is the UTF-8 JSON of the anchored chain heads, recorded only once mined. |
Standings are separate on purpose
| verdict | values | answers |
|---|---|---|
| integrity | digest, chain link, signature | Was this record altered after it was written? |
| proof | none · unconfirmed · chain-confirmed · chain-contradicted | Did anything happen on a chain? Contradicted, a hash that exists but disagrees, is worse than none. |
| settlement | not-evaluated · unconfirmed · confirmed-unverified · mismatched · matched | Did amount, asset and recipient on chain equal what was authorised? |
| anchor | anchored or not, with tx and calldata | Could Ava have rewritten history at or before this sequence? |
Design decisions
- Kind is primary. Lend, bridge, stake and perp go to their own protocol adapters and never fall through to a swap venue. A request naming a venue the chain does not carry is LEND_VENUE_MISMATCH, not a reroute.
- Callable and proven are different claims. The registry keeps mainnetProven apart from implemented, apiReachable and operatorEnabled. A route that once settled and is switched off is not callable.
- One implementation, two transports. MCP tools call the same functions as REST routes so a gate cannot exist on one and not the other.
- Never invent. No generated idempotency keys, no default venues, no placeholder addresses, no sample history on an empty track record.
- Ava cannot authorise itself. Plan and standing signatures from any address in AVA_CONTROLLED_SIGNERS are refused, and no process mints its own agent credential.
- Debit after broadcast never throws. A failed standing debit is logged, not returned as a 5xx, because a retry after a 5xx would pass the consult again and spend twice.
Known gaps
Found while tracing the code for these pages. Each is stated as the code has it on the date of writing, so it can be checked and closed.
| gap | where | consequence |
|---|---|---|
| MCP live lend has no single-flight claim | routes/copilot.ts → McpDeps | The MCP transport passes no executionClaims to ava_lend_execute, so duplicate MCP calls are not de-duplicated by the claim store. Policy reservations still apply. |
| REST claims depend on the caller's key | lib/reserve-execution.ts | Without an idempotencyKey in the body, /v1/lend/execute runs unreserved by design. Safe from fabricated keys, but retries are only protected when clients send one. |
| Idempotency-Key cache is per process | app.ts, idempotency middleware | Replays of /v1/intents and /v1/execute are recognised only by the replica that served them, and are forgotten on restart. |
| Automations are not durable | runtime.ts | InMemoryAutomationStore is used in Postgres mode too, so automations vanish on deploy while mandates survive. |
| Standing rotations do not run unattended | index.ts | No execute function is bound to the scheduler, so a signed standing envelope is evaluated but never rotated by the tick. Rotations happen only when a caller invokes lend with the envelopeId. |
| Receipt signing depends on configuration | AVA_RECEIPT_SIGNING_KEY | Whether production receipts are Ed25519-signed, and whether anchoring is enabled, is environment state the repository cannot show. /v1/receipts/:ref/verify reports both per receipt. |
| README lags the workflow route | README.md | The README says multi-leg execution is not wired. POST /v1/workflows/execute now admits, reserves and runs legs; most legs still refuse at the registry or the credential gate. |
| Several deployment targets | fly.toml · vercel.json · railway.toml | Three configurations can boot the API. Fly.io is the documented production; the others are unverified and could drift. |
For what executes right now, read the live registry in the reference rather than any prose, including this page.