architecture · workflows
Core workflows, end to end
The paths that decide whether Ava is trustworthy, drawn as they run in apps/api-worker today. Each diagram names the real route or tool, the store it writes and the refusal a caller meets when a check fails.
Reading these diagrams
- Solid arrow with a filled head is a synchronous call that the caller waits on. A dashed arrow is its response.
- alt / else blocks are branches the code actually takes; opt is a step that only happens sometimes; loop repeats; par runs independently of the request.
- Refusal codes in the diagrams are the literal error.code values. The full list is in the reference.
- Every MCP tool and REST route on these paths shares one implementation, so the MCP and HTTP versions of a flow differ only in transport.
Onboarding an agent
A stranger goes from nothing to a funded-ready Base address in one command. The CLI drives the public ava_session tool, then two authenticated tools. Source: packages/connect/src/connect.ts, apps/api-worker/src/routes/users.ts, packages/wallet-turnkey/src/sub-organization.ts.
Rendering diagram
sequenceDiagram
autonumber
actor Dev as Developer
participant CLI as connect CLI
participant MCP as API /mcp
participant Ten as Tenancy store
participant Tok as Session tokens
participant TK as Turnkey
Dev->>CLI: npx @getava-xyz/connect
CLI->>MCP: tools/call ava_session
Note over MCP: Public tool. No credential needed.
MCP->>Ten: create user usr_…
MCP->>Tok: issue(userId, audience spend)
Tok-->>MCP: ava_st_ token, stores SHA-256 digest only
MCP-->>CLI: userId, token shown once
CLI->>CLI: write HTTP MCP config, file mode 0600<br/>Claude Code · Cursor · Codex
CLI->>MCP: ava_create_agent byo-external + Bearer
MCP->>MCP: auth middleware resolves principal<br/>userId argument must match it
MCP->>Ten: create agent instance
MCP-->>CLI: agentInstanceId
CLI->>MCP: ava_provision_wallet family evm + Bearer
MCP->>Ten: ensure wallet row, status pending_provision
MCP->>TK: get or create the user's sub-organization
MCP->>TK: create wallet in that sub-organization
TK-->>MCP: EVM address
MCP->>Ten: save address, status active
MCP-->>CLI: wallet address
alt wallet still pending
CLI-->>Dev: stop with WALLET_PENDING_PROVISION<br/>no invented address
else active
CLI-->>Dev: print Base address and fund USDC + gas
endEach user's wallets are created in that user's own Turnkey sub-organization, so one user's growth can never exhaust another's wallet allowance. The CLI never signs; it only reads back an address.
Creating the session needs no CAPTCHA, email or wallet popup. That is a product decision, and it is why every other user-scoped call checks the bearer token and treats any userId argument as a claim to compare, never as identity.
Mandates
A mandate is the job written down before the agent acts: objective, capital, constraints and the conditions that end the position. Every live lend must name one, and every receipt carries its hash. Source: apps/api-worker/src/routes/mandates.ts, packages/mandates.
Rendering diagram
sequenceDiagram
autonumber
participant Agent
participant API as API /v1/mandates
participant Reg as Capability registry
participant Store as Mandate store
participant PG as Postgres
participant CG as CoinGecko
Agent->>API: POST /v1/mandates objective, capital, constraints<br/>or a natural-language message
API->>API: zod validation, else 422 MANDATE_VALIDATION_FAILED
opt message supplied
API->>API: parseMandateMessage, else 422 MANDATE_PARSE_FAILED
end
API->>Reg: resolve the action for objective + capital
API->>Store: create mandate, status active
Store->>PG: write ava_mandates
API->>Store: flush, response waits for durability
API-->>Agent: 201 mandate + mandateHash
Agent->>API: POST /v1/mandates/:id/evaluate
API->>Store: load, owner check, else 404
alt no snapshot in body and conditions exist
API->>CG: live price for the condition asset
CG-->>API: price, change pct
end
API->>API: evaluateMandate
API->>Store: recordEvaluation, maybe status triggered
API-->>Agent: matched, evaluation, snapshot<br/>actionDispatched falseEvaluation never dispatches an action. It records what it observed and may move the mandate to triggered; spending is a separate, gated call.
The mandate hash covers the terms only (id, user, portal, objective, capital, constraints and createdAt), so later status changes and evaluations cannot break a receipt’s binding to the terms it was issued under. Mandates can also be signed by the user over EIP-712 through /v1/mandates/:id/signing-payload and /signature.
Live lend, two-phase
This is the path that has settled real transactions on mainnet, and the one every other executor copies. Morpho on Base is drawn here. Aave v3 on Monad, Avalanche, BNB Chain and Arbitrum runs the same ordered gates in lib/aave-lend-execute.ts and adds one step: it re-reads the Aave Pool’s own Supply or Withdraw event and checks it against the authorisation before reporting a fill.
Rendering diagram
sequenceDiagram autonumber actor Human participant Agent as Coding agent participant MCP as ava_lend_execute participant Pipe as executeMorphoLend participant Gates as Gates participant RPC as Base RPC Agent->>MCP: mandateId, amount<br/>Bearer ava_st_ MCP->>MCP: spend audience?<br/>else SPEND_CREDENTIAL_REQUIRED MCP->>Pipe: execute, no previewHash Pipe->>Gates: allowed kinds, chain, registry route Pipe->>RPC: read position and balances Pipe->>Pipe: build preview<br/>previewHash = sha256(pre-image) Pipe->>Gates: mandate gate, then policy gate Note over Gates: Mainnet confirm threshold is 0 USD,<br/>so no matching previewHash means held Gates-->>Pipe: POLICY_HUMAN_CONFIRMATION_REQUIRED Pipe-->>MCP: refusal carrying preview + previewHash MCP-->>Agent: refused + previewHash<br/>re-send once a human approves Agent->>Human: show the exact preview Human-->>Agent: approve
The first call returns the preview as a refusal. Ava never treats the agent's call as the human's confirmation.
Rendering diagram
sequenceDiagram
autonumber
participant Agent as Coding agent
participant Pipe as executeMorphoLend
participant Gates as Gates and reservations
participant RPC as Base RPC
participant TK as Turnkey
participant Led as Receipt ledger
Agent->>Pipe: same call + previewHash, via ava_lend_execute
Pipe->>RPC: re-read state, recompute previewHash
Pipe->>Gates: registry, mandate, policy<br/>humanConfirmed only if hashes match
alt any gate refuses
Gates-->>Agent: typed refusal, filled false
end
Pipe->>Gates: claim idempotency key<br/>when a key and store exist
Gates-->>Pipe: reserved, or 409 EXECUTION_ALREADY_CLAIMED
Pipe->>Gates: reserve daily notional + exposure
Note over Pipe,TK: Nothing above this point can produce a signature
Pipe->>RPC: balance pre-check<br/>else LEND_INSUFFICIENT_BALANCE
Pipe->>TK: sign exact-amount approve<br/>only if allowance is short
Pipe->>RPC: broadcast approve
Pipe->>TK: sign ERC-4626 deposit<br/>from must equal the wallet
Pipe->>RPC: broadcast deposit
loop until confirmed or timeout
Pipe->>RPC: getTransactionReceipt
end
alt confirmed
Pipe->>RPC: re-read tx, compare status,<br/>token, amount, sender, recipient
Pipe->>RPC: position pinned to the confirmed block
Pipe->>Led: write receipt<br/>chained + signed at the boundary
Pipe-->>Agent: FILLED, txHash, verifyUrl<br/>mayClaimFill true
else confirmation timeout
Pipe->>Led: receipt, proof unconfirmed
Pipe-->>Agent: unconfirmed, not a fill<br/>re-check the hash before retrying
endThe second call is the same tool carrying the approved hash. Every gate runs again against fresh chain state before a signer exists.
Why the preview hash, not a flag
humanConfirmed is derived by recomputing the preview from live chain state and comparing it with the hash the caller sends. A boolean in the request would let any agent confirm on the human’s behalf. A hash that no longer matches, because the position or the amount changed, simply produces a fresh preview to approve.
The order of the gates
| step | question | refuses with |
|---|---|---|
| 1 kind | Does the mandate allow lend, on this chain? | MANDATE_KIND_NOT_ALLOWED · LEND_CHAIN_UNSUPPORTED |
| 2 registry | Is this chain, asset and venue executable here? | 409 with the registry code |
| 3 preview | What exactly would happen, from real state? | chain read failures |
| 4 mandate | Owned, active, within capital and constraints? | MANDATE_* codes |
| 5 policy | Allowlists, limits, expiry, human confirmation? | POLICY_REJECTED · POLICY_HUMAN_CONFIRMATION_REQUIRED |
| 5b claim | Has this exact action already been reserved? | EXECUTION_ALREADY_CLAIMED |
| 5c budget | Is there daily notional and exposure left? | MORPHO_* budget codes |
| 6 sign | Only now is a Turnkey signer built. The executor checks the wallet balance first. | LEND_INSUFFICIENT_BALANCE |
| 7 receipt | What did the chain actually do? | proof standing unconfirmed |
Dashboard device link
The hosted dashboard cannot read a developer’s local MCP config, and pasting a spend token into a browser is exactly what the design avoids. A short-lived, single-use device code bridges the two and yields a weaker, view-only credential. Source: lib/device-codes.ts, routes/users.ts, apps/site/src/components/dashboard.
Rendering diagram
sequenceDiagram
autonumber
actor Human
participant Local as connect dashboard<br/>on the dev machine
participant API as Ava API
participant Codes as Device codes
participant Site as getava.xyz/dashboard
Human->>Local: npx @getava-xyz/connect dashboard
Local->>Local: discover existing local sessions<br/>no new user is minted
Local->>API: POST /v1/users/me/device, Bearer ava_st_
API->>Codes: issue ava_dc_ code, 10 minute TTL
API-->>Local: dashboard URL with #device=ava_dc_…
Note over Local,Site: The code sits in the URL fragment, so it is never<br/>sent to a server, logged, or forwarded as Referer
Local-->>Human: open link
Human->>Site: open /dashboard#device=…
Site->>API: POST /v1/users/session/device with code
API->>Codes: consume, single use
alt unknown, expired or used
API-->>Site: 401 DEVICE_CODE_INVALID
else valid
API->>API: issue token with audience dashboard
API-->>Site: ava_dt_ token, same userId
Site->>API: GET /v1/users/me, mandates, records
Note over Site,API: A dashboard token views. Every live-spend route<br/>answers 403 SPEND_CREDENTIAL_REQUIRED
endSpend authority stays on the original ava_st_ credential. The browser only ever holds an ava_dt_ token.
Multi-leg workflows
One sentence can describe several dependent actions across chains. Planning is free and never signs; execution re-derives the plan, checks the user’s signature over it, admits every leg, and only then runs legs in dependency order. Source: routes/workflows.ts, lib/workflow-execute.ts, lib/workflow-runner.ts, packages/intent.
Rendering diagram
sequenceDiagram
autonumber
actor User as User wallet
participant Agent
participant API as /v1/workflows
participant Int as decomposeWorkflow
participant Reg as Capability registry
Agent->>API: POST /plan message
alt message describes repeated action
API-->>Agent: 409 WORKFLOW_IS_STANDING, use /v1/workflows/standing
end
API->>Int: decompose into legs with dependsOn
Int-->>API: legs, unsupported clauses, budget
API->>Reg: per-leg executability
API->>API: planHash over the canonical legs + userId
alt every money leg executable and within budget
API-->>Agent: status planned, legs, EIP-712 typedData
else not signable
API-->>Agent: status planned, planHash null, reasons
end
Agent->>User: request signature over typedData
User-->>Agent: signatureRendering diagram
sequenceDiagram
autonumber
participant Agent
participant API as POST /v1/workflows/execute
participant Int as decomposeWorkflow
participant Sig as Plan signature check
participant Adm as admitWorkflow
participant Run as executeWorkflowPlan
participant Leg as Leg executors
participant Chain as Chains
Agent->>API: message, signature, previewHashes per leg
API->>API: spend audience required
API->>Int: re-plan from the original message
Note over API,Int: Legs are never taken from the request,<br/>so a signature cannot authorize a different plan
API->>Sig: verify against recomputed planHash
alt signer invalid or Ava-controlled
Sig-->>Agent: 403, nothing moved
end
API->>Adm: every leg kind bound and registry-executable
alt any leg cannot run
Adm-->>Agent: 409 refused, per-leg reasons, nothing moved
end
API->>Run: legs in dependency order
loop each leg whose dependencies are verified
Run->>Run: reserve claim per leg idempotency key
Run->>Leg: execute with amount handed off
Leg->>Chain: sign, broadcast, wait for finality
Chain-->>Leg: settled
Leg-->>Run: verified with receivedBase
Note over Run: A bridge leg passes on the amount<br/>actually delivered, not the amount sent
end
alt every leg verified
Run-->>Agent: completed, per-leg tx hashes
else a leg failed
Run-->>Agent: failed, dependents skipped, never reported as success
endAdmission is all-or-nothing: a plan with one unexecutable leg is refused before any leg runs, so a workflow can never be half-started by design.
Standing authorization
For instructions the user will not be awake to approve, the user signs bounds once instead of every rotation: venues, chains, asset, per-move and cumulative caps, frequency and expiry. Any edit to a bound after signing breaks the signature. Source: lib/standing-lifecycle.ts, packages/mandates/src/standing*.ts, apps/execution-worker/src/standing-tick.ts.
Rendering diagram
sequenceDiagram
autonumber
actor User as User wallet
participant Agent
participant API as /v1/workflows/standing
participant Env as Envelope store
participant Rev as Revocations
participant Sch as Scheduler tick
participant Lend as /v1/lend/execute
Agent->>API: POST /standing, every hour rotate USDC…
API-->>Agent: draft envelope + list of bounds not specified
Agent->>API: POST /standing/digest envelope, chainId
API-->>Agent: EIP-712 typedData and digest
Agent->>User: sign the bounds once
User-->>Agent: signature
Agent->>API: POST /standing/authorize envelope, signature
API->>API: owner check, signature check,<br/>refuse an Ava-controlled signer
API->>Env: store envelope, tickEnabled only if asked
API-->>Agent: authorized, rotationsScheduled false
loop every 60 s
Sch->>Env: due envelopes
Sch->>Rev: revoked?
Note over Sch: Production binds no execute function,<br/>so each due envelope is recorded as<br/>evaluated with nothing run
end
Agent->>Lend: lend with envelopeId, venue, amount
Lend->>Rev: consult revocation
Lend->>Env: checkStandingMove against durable consumed + moves today
alt outside the signed bounds
Lend-->>Agent: 403 with the violated bound
else inside
Lend->>Lend: full lend pipeline, same gates as one-shot
Lend->>Env: debit consumed after broadcast, never throws
Lend-->>Agent: result
end
Agent->>API: POST /standing/revoke
API->>Rev: revoke, terminal with no un-revokeConsumed amounts and move counts are read from durable rows, not from the request, so a restart or a second replica cannot hand the cap back.
/v1/workflows/standing/simulate answers whether a specific rotation would be permitted without doing it, which is how a user can test the bounds before trusting them with capital.
Receipt verification
Anyone holding a receipt hash can check it without an Ava account. The check is split into separate verdicts on purpose: a receipt can be well-formed, chained, signed and confirmed on chain, and still describe a settlement that differs from what was authorised. Source: routes/receipt-verify.ts, lib/receipt-chain.ts, lib/receipt-anchor.ts, packages/verifiability.
Rendering diagram
sequenceDiagram
autonumber
participant V as Verifier
participant API as GET /v1/receipts/:ref/verify
participant Led as Receipt ledger
participant Vis as Receipt visibility
participant Man as Mandate store
participant Anc as Anchor store
V->>API: ref = 64-hex receiptHash or executionId
alt receiptHash
API->>Led: resolve by hash, no credential needed
else executionId
API->>Led: get by executionId
alt caller is not the owner
API->>Vis: has the owner opted this receipt public?
alt not public
API-->>V: 404 RECEIPT_NOT_FOUND
else public
API-->>V: redacted public verification
end
end
end
API->>API: recompute receiptHash, invariants,<br/>chain link, signature
API->>Man: recompute mandateHash<br/>check EIP-712 mandate signature
API->>Anc: is this sequence covered by an anchor?
API-->>V: verified, integrity, proof, settlement, anchor<br/>+ howToVerify recipe per checkRendering diagram
sequenceDiagram
autonumber
participant Sch as Scheduler
participant Led as Receipt ledger
participant TK as Turnkey anchor key
participant Base as Base chain
participant Anc as Anchor store
loop every AVA_RECEIPT_ANCHOR_INTERVAL_MS
Sch->>Led: chain heads moved since the last anchor?
alt nothing moved
Sch->>Sch: skip
else heads moved
Sch->>TK: sign self-send, calldata = JSON heads<br/>chainId · sequence · receiptHash
Sch->>Base: broadcast
Base-->>Sch: mined in block N
Sch->>Anc: record anchor only after confirmation
end
endOut of band, on the scheduler. Anchoring never blocks a receipt write. An anchor stops receipts at or below its sequence from being rewritten; it does not prove that a receipt exists for every action.
How each field is computed and what each standing means is on Data and trust.
Copilot turn
The chat copilot in the operator console predates the MCP lend loop. Over MCP it is advertised as testnet-only, and it remains the only producer of the executionId that ava_preview_tx and ava_approve_execute consume. Source: lib/copilot.ts, lib/paper-approve.ts, lib/live-approve.ts, apps/agent-service.
Rendering diagram
sequenceDiagram
autonumber
participant UI as Console or agent
participant API as /v1/copilot
participant NL as Heuristic parser
participant OR as OpenRouter
participant Pol as Planning policy
participant Brk as Broker router
participant Plans as Pending plans
participant Paper as Paper portfolio
UI->>API: POST /turn message, portal, mode
API->>NL: parse
alt heuristic miss and message is trade-shaped
API->>OR: structured AvaIntent, 2.5 s deadline
OR-->>API: intent or nothing
end
API->>Pol: evaluate planning policy
API->>Brk: route by settlement preference
Brk-->>API: quote or dry-run draft, CoW · Jupiter · Cetus
API->>Plans: register pending plan
API-->>UI: reply, plan, executionId, actions
UI->>API: POST /approve executionId, spend credential
API->>API: mandate gate if the plan names a mandate
alt testnet mode
API->>Paper: settle against paper balances
API-->>UI: paper fill, no chain touched
else mainnet mode
API->>API: live readiness, preview, policy and wallet gates
API-->>UI: live result or typed refusal
endThe model is optional and only translates words into a structured intent. It never chooses to sign; every gate after parsing is deterministic code.