Ava

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.

From request to verifiable record
diagram

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 askindaudiencenotes
ava_st_…Session tokenspendMinted by POST /v1/users/session or ava_session. Shown once; only its SHA-256 digest is stored.
ava_dt_…Dashboard tokenviewMinted only by exchanging a device code. Authenticates, cannot reach any live-spend route or tool.
ava_dc_…Device codeexchangeTen-minute, single-use. Travels in a URL fragment so it is never logged by a server.
API keyAVA_API_KEYSspendBound to exactly one userId at boot. There is no key that can act as an arbitrary tenant.
x-ava-agent-credentialScoped agent credentialper scopeIssued 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-tokenOperator tokenoperatorPause-all, resume-all, per-execution pause and revoke, usage. Routes are disabled when AVA_OPERATOR_TOKEN is unset.
x-ava-user-idAssertionnoneA 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.

Core Postgres tables
diagram

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

datalives inwhere
Users, agents, walletsPostgresava_tenancy_*
Session and dashboard tokens, device codesPostgresava_auth_tokens, ava_device_codes
Mandates and refusalsPostgres, with an in-memory cache hydrated at bootava_mandates, ava_mandate_refusals
Receipts and their chainPostgresava_receipts, ava_receipt_chain
Receipt and record visibilityPostgresava_receipt_visibility, ava_record_visibility
AnchorsPostgresava_receipt_anchors, ava_receipt_anchor_heads
Execution claims, nonce leases, budgetsPostgresava_execution_claims, _nonce_leases, _budgets
Daily notional and exposure reservationsPostgresava_policy_reservations
Standing envelopes and revocationsPostgresava_standing_envelopes, ava_standing_revocations
Principal challenges, proofs, agent credentialsPostgresava_principal_*, ava_agent_credentials
Workflow ledgerPostgresava_executions, ava_domain_events, ava_outbox, ava_idempotency
HTTP Idempotency-Key replay cacheProcess memoryMap in createApp
AutomationsProcess memoryInMemoryAutomationStore, even in Postgres mode
Copilot pending plans, previews, paper balancesProcess memorylost on restart
Policy state read by the policy gateProcess memorythe 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.

Hash chain, signature, anchor
diagram

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"]
fieldhow it is computed
receiptHashsha256 of the RFC 8785 canonical JSON of the receipt, excluding its own hash and signatures.
prevReceiptHashThe receiptHash of the previous receipt in the same chain id.
integrity.signatureEd25519 over the lowercase hex of receiptHash, by AVA_RECEIPT_SIGNING_KEY. Absent key: an unsigned reason is recorded instead.
mandateHashsha256 of the canonical mandate terms: id, user, portal, objective, capital, constraints, createdAt.
gate.digestsha256 of the canonical gate pre-image, so the decision to sign is replayable.
anchorA Base transaction whose calldata is the UTF-8 JSON of the anchored chain heads, recorded only once mined.

Standings are separate on purpose

verdictvaluesanswers
integritydigest, chain link, signatureWas this record altered after it was written?
proofnone · unconfirmed · chain-confirmed · chain-contradictedDid anything happen on a chain? Contradicted, a hash that exists but disagrees, is worse than none.
settlementnot-evaluated · unconfirmed · confirmed-unverified · mismatched · matchedDid amount, asset and recipient on chain equal what was authorised?
anchoranchored or not, with tx and calldataCould Ava have rewritten history at or before this sequence?
Only a receipt with proof standing chain-confirmed counts as a fill, in an agent’s track record or in an MCP result. Over MCP, only the filled outcome may carry a transaction hash and set mayClaimFill: true, so a model cannot narrate a fill it has no hash for.

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.

gapwhereconsequence
MCP live lend has no single-flight claimroutes/copilot.ts → McpDepsThe 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 keylib/reserve-execution.tsWithout 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 processapp.ts, idempotency middlewareReplays of /v1/intents and /v1/execute are recognised only by the replica that served them, and are forgotten on restart.
Automations are not durableruntime.tsInMemoryAutomationStore is used in Postgres mode too, so automations vanish on deploy while mandates survive.
Standing rotations do not run unattendedindex.tsNo 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 configurationAVA_RECEIPT_SIGNING_KEYWhether 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 routeREADME.mdThe 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 targetsfly.toml · vercel.json · railway.tomlThree 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.