Ava

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.

npx @getava-xyz/connect
diagram

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
  end

Each 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.

Create and evaluate a mandate
diagram

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 false

Evaluation 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.

Phase 1 · preview and human approval
diagram

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.

Phase 2 · gates, signature, settlement
diagram

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
  end

The 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

stepquestionrefuses with
1 kindDoes the mandate allow lend, on this chain?MANDATE_KIND_NOT_ALLOWED · LEND_CHAIN_UNSUPPORTED
2 registryIs this chain, asset and venue executable here?409 with the registry code
3 previewWhat exactly would happen, from real state?chain read failures
4 mandateOwned, active, within capital and constraints?MANDATE_* codes
5 policyAllowlists, limits, expiry, human confirmation?POLICY_REJECTED · POLICY_HUMAN_CONFIRMATION_REQUIRED
5b claimHas this exact action already been reserved?EXECUTION_ALREADY_CLAIMED
5c budgetIs there daily notional and exposure left?MORPHO_* budget codes
6 signOnly now is a Turnkey signer built. The executor checks the wallet balance first.LEND_INSUFFICIENT_BALANCE
7 receiptWhat did the chain actually do?proof standing unconfirmed
The single-flight claim in step 5b runs only when the caller supplies an idempotencyKey and a claim store is wired. Ava never invents a key, because a generated key would make every retry look new. The REST route passes the Postgres claim store; the MCP transport does not wire one today. See known gaps.

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.

Device code → dashboard token
diagram

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
  end

Spend 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.

Plan and sign
diagram

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: signature
Execute a signed plan
diagram

Rendering 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
  end

Admission 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.

The route is wired, but most legs refuse today. Swap legs need a scoped agent credential (x-ava-agent-credential, issued by /v1/principals/verify) and a registry row that is operator-enabled; the Across bridge is wired with its operator switch off. Per-leg previewHashes are still required: the plan signature authorizes which actions may run, not that a human saw each exact artifact.

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.

Authorize bounds, consult on every move
diagram

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-revoke

Consumed 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.

Verify a receipt
diagram

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 check
Anchor the receipt chain
diagram

Rendering 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
  end

Out 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.

Copilot turn and approve
diagram

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
  end

The model is optional and only translates words into a structured intent. It never chooses to sign; every gate after parsing is deterministic code.