Ava

architecture

How Ava is built

One API process holds every rule that stands between an agent and a user’s capital. This page maps that process, what surrounds it, and where each responsibility lives in the repository. Every diagram was drawn from the source in apps/ and packages/, not from the roadmap.

What Ava is

Ava is the execution layer for agents that do DeFi. A coding agent can describe what it wants done with money, but it holds no key, no signer and no RPC. Ava holds those, and it holds them behind rules the agent cannot change: a mandate the user agreed to, a policy envelope, a registry of what is actually executable, and a receipt that separates what was claimed from what a chain confirmed.

The whole system is one spine, and every live spend walks it in order:

  1. 01

    Intent. An agent names an action through MCP or REST: supply 5 USDC to Morpho on Base, under mandate mdt_…

  2. 02

    Authority. The caller's bearer token must carry the spend audience, and the mandate must belong to that caller.

  3. 03

    Executability. The capability registry decides whether that kind, chain and venue may run on this deployment.

  4. 04

    Preview. Ava reads real chain state and builds the exact action. Its hash is what a human approves.

  5. 05

    Gates. Mandate gate, policy gate, a single-flight execution claim and a durable budget reservation.

  6. 06

    Signature. Only now is a signer built. Turnkey signs with the user's own key, in the user's own sub-organization.

  7. 07

    Settlement. Ava broadcasts, waits for the chain, and compares what settled with what was authorised.

  8. 08

    Receipt. A hash-chained receipt is written. Only a chain-confirmed settlement may be reported as a fill.

Execution is fail-closed by design. A missing signature, approval, adapter, credential or operator switch is a typed refusal with filled: false, never a default and never a silent reroute to another venue.

System context

Every caller reaches the API through the getava.xyz origin. Agents speak MCP as JSON-RPC over HTTP; browsers and the CLI also use REST. The site forwards /v1, /mcp and /health to the API on Fly.io, which is the only component that talks to Postgres, Turnkey and the chains.

System context
diagram

Rendering diagram

flowchart LR
  subgraph callers["Callers"]
    direction TB
    agent["Coding agent<br/>Claude Code · Cursor · Codex"]
    claw["OpenClaw skill"]
    cli["npx @getava-xyz/connect"]
    human["Human in a browser"]
    verifier["Third-party verifier"]
  end

  subgraph edge["getava.xyz · Vercel"]
    direction TB
    site["Next.js site<br/>docs · install · dashboard"]
    console["Operator console<br/>Vite SPA at /console"]
  end

  subgraph core["Ava API · Fly.io"]
    direction TB
    api["api-worker<br/>Hono · REST + MCP"]
    sched["Scheduler<br/>same process"]
  end

  pg[("Postgres<br/>ledger · tenancy · claims")]
  turnkey["Turnkey<br/>per-user sub-org keys"]
  chains["EVM + Sui RPC<br/>Base · Monad · Avalanche · BNB · Arbitrum"]
  venues["Venue + data APIs<br/>Kyber · 0x · LI.FI · CoW · Jupiter<br/>CoinGecko · DefiLlama"]
  llm["OpenRouter<br/>optional, copilot only"]

  agent -- "MCP over HTTP" --> site
  claw -- "MCP over HTTP" --> site
  cli -- "MCP + REST" --> site
  human --> site
  human --> console
  verifier -- "GET /v1/receipts/:hash/verify" --> site
  site -- "rewrite /v1 /mcp /health" --> api
  console -- "/v1 via site origin" --> api
  api --> pg
  sched --> pg
  api -- "sign" --> turnkey
  api -- "read · broadcast · confirm" --> chains
  sched -- "receipt anchors" --> chains
  api -- "quotes · prices · yields" --> venues
  api -.-> llm

Solid arrows are synchronous calls. The dashed OpenRouter edge is optional: the copilot tries a deterministic parser first and calls the model only for trade-shaped messages.

The proxy exists for a concrete reason recorded in apps/site/next.config.ts: two of the domain’s four nameservers answered NXDOMAIN for api.getava.xyz, so roughly half of visitors could not resolve the API host. Serving the API from the site’s own origin removed that dependency and the CORS preflight with it.

Deployables

The monorepo builds eight things that ship. Only one of them holds state or keys. Two that look like services by name, agent-service and execution-worker, are libraries the API imports and runs in its own process.

pathbuilt withruns onresponsibility
apps/api-workerHono on Node 22Fly.io, always onThe product. Every REST route, the MCP server, every gate, every executor and the scheduler run in this one process.
apps/siteNext.js 16, React 19, Tailwind 4Vercelgetava.xyz: landing, install guide, these docs, the hosted dashboard. Proxies /v1, /mcp and /health to the API so browsers never cross origins.
apps/webVite SPA, no frameworkVercel, mounted at /consoleThe operator console and chat copilot. Reached through the site's rewrites so a user never leaves getava.xyz.
packages/connectNode CLI on npmnpx @getava-xyz/connectCreates a session over MCP, writes HTTP MCP config for Claude Code, Cursor and Codex, registers a byo-external agent, provisions a Base wallet.
packages/openclaw-skillSkill packopenclaw skills installThe same MCP loop packaged for OpenClaw.
apps/hud-daemonNode CLIRuns on the developer's machineReceives coding-agent hook events and binds each terminal session to an Ava agent instance for the local dashboard and status line.
apps/agent-serviceLibrary (Mastra)In-process, imported by api-workerStructured intent extraction, policy explanation and CoW dry-run drafts for the copilot. It never signs.
apps/execution-workerLibraryIn-process, imported by api-workerThe standing-rotation tick, the CoW transport and the Turnkey signature-request port. Not a separately deployed worker today.

Stack

concernchoice
LanguageTypeScript 5.7 everywhere, ESM, strict with exactOptionalPropertyTypes in the execution packages.
Workspacepnpm 9 workspaces; turbo.json declares the build, typecheck and test graph. 49 packages under packages/, 7 apps under apps/.
HTTPHono 4 with @hono/node-server. One app factory, createApp(), wired by runtime.ts.
Validationzod at every request boundary. A body that does not parse is a 422 with the issues attached.
PersistencePostgres 16 through node-postgres. Schemas are applied idempotently at boot; there is no separate migration runner.
KeysTurnkey via @turnkey/sdk-server. Each user's wallets live in that user's own Turnkey sub-organization.
ChainsHand-built EIP-1559 serialization and JSON-RPC clients per chain family; @mysten/sui for Sui.
TestsVitest across packages (about a thousand tests), PGlite for in-process Postgres, Anvil forks in testkit-fork.

API worker layers

Inside the API there are two transports and one implementation. REST routes live in apps/api-worker/src/routes; the MCP server in lib/mcp.ts dispatches each tool to the same functions those routes call. This is deliberate: a second implementation behind MCP is how the mandate gate was once enforced over HTTP and skipped over MCP.

Layers of apps/api-worker
diagram

Rendering diagram

flowchart TB
  subgraph transport["Transport"]
    direction LR
    rest["REST routes<br/>routes/*.ts"]
    mcp["MCP JSON-RPC<br/>lib/mcp.ts · 29 tools"]
    rest ~~~ mcp
  end

  mw["Middleware chain<br/>CORS · request id · logging · auth · spend audience · idempotency"]

  subgraph domain["Domain"]
    direction LR
    tenancy["Sessions · agents · wallets<br/>@ava/tenancy"]
    mandates["Mandates · standing envelopes<br/>@ava/mandates"]
    planner["Intent + workflow planner<br/>@ava/intent"]
    copilot["Copilot<br/>lib/copilot.ts"]
    tenancy ~~~ mandates ~~~ planner ~~~ copilot
  end

  subgraph gates["Gates, in the order a spend meets them"]
    direction LR
    registry["Capability registry<br/>implemented · apiReachable · operatorEnabled"]
    mgate["Mandate gate"]
    policy["Policy gate<br/>@ava/policy"]
    claim["Execution claim<br/>single flight"]
    budget["Policy reservation<br/>daily + exposure"]
    registry --> mgate --> policy --> claim --> budget
  end

  subgraph exec["Execution"]
    direction LR
    lend["Lend executors<br/>Morpho · Aave · Compound · Suilend"]
    swap["Swap venues<br/>Kyber · 0x · Uniswap v3 · LI.FI"]
    bridge["Across bridge"]
    lend ~~~ swap ~~~ bridge
  end

  subgraph ports["Ports"]
    direction LR
    signer["Turnkey signer"]
    rpc["RPC transport"]
    ledger["Receipt ledger<br/>chain + sign at write"]
    signer ~~~ rpc ~~~ ledger
  end

  transport --> mw --> domain
  domain --> gates --> exec --> ports

A request descends one way. Nothing in the execution layer can produce a signature until every gate above it has passed, and the signer is constructed after the gates rather than injected before them.

What each layer owns

  • Transport. Parses and validates input with zod, resolves the portal, and returns the shared envelope { ok, …, requestId }. MCP narrows what tools/list advertises to an 18-tool default (opt in to more with ?toolsets=) because Cursor caps a user at 40 tools across all servers; tools/call still accepts every tool by name.
  • Domain. Tenancy maps a user to agent instances and wallet families (evm, solana, sui). Mandates hold objective, capital, constraints and exit conditions, and are hashed over their terms only, so status changes never break a receipt’s binding.
  • Gates. Each gate answers a different question: may this kind run here, does the mandate allow it, does policy allow it, has it already been claimed, and is there budget left today. They are ordered so a refused request never consumes a reservation.
  • Execution. One executor per venue family, each with its own settlement check. Aave re-reads the Pool’s own Supply event; Compound reads Comet.balanceOf in the settling block; Morpho reads the ERC-4626 position pinned to the confirmed block.
  • Ports. The ledger wrapper chains and optionally signs every receipt at the storage boundary, so no writing path can forget to.

Request pipeline

Every request crosses the same middleware chain, registered in apps/api-worker/src/app.ts. Authentication is deny-by-default: a short allowlist of public routes answers anonymously and everything else needs a credential.

Middleware chain
diagram

Rendering diagram

flowchart TB
  req(["HTTP request"]) --> pre["CORS allowlist · inject stores<br/>request id · structured log"]
  pre --> auth{"Credential presented?"}
  auth -- "invalid" --> e401a["401 UNAUTHORIZED"]
  auth -- "none, private path" --> e401b["401 AUTH_REQUIRED"]
  auth -- "valid API key or session token" --> principal["Principal set<br/>userId · kind · audience"]
  auth -- "none, public path" --> spend
  principal --> spend{"Live-spend path?"}
  spend -- "audience is not spend" --> e403["403 SPEND_CREDENTIAL_REQUIRED"]
  spend -- "allowed" --> idem{"Mutation that needs<br/>an Idempotency-Key?"}
  idem -- "key missing" --> e422["422 IDEMPOTENCY_KEY_REQUIRED"]
  idem -- "key seen before" --> replay["Replay the cached 2xx"]
  idem -- "proceed" --> route["Route handler"]
  route --> envelope["Any thrown ApiError becomes<br/>ok false · error · requestId"]

Spend-audience checks run before idempotency on purpose, so a dashboard token on a spend route is told 403 rather than a misleading 422 for a missing header.

Public routes

GET /health, GET /v1/capabilities, GET /v1/portal, GET /v1/portal/catalog, POST /v1/users/session, POST /v1/users/session/device, /mcp and /v1/mcp (each tool authenticates itself), GET /v1/agents/:id/record (handler still hides owner-only records) and GET /v1/receipts/:ref/verify.

Live-spend routes

POST /v1/lend/execute, /v1/workflows/execute, /v1/copilot/approve, /v1/execute/:id and /v1/bridge/execute refuse any principal that cannot spend. Over MCP, the same rule is applied per tool to ava_lend_execute and ava_approve_execute. The credential model is on Data and trust.

Package graph

The 49 workspace packages fall into six groups. Dependencies point downward toward a small set of shared contracts, which is what lets an adapter be tested in isolation and replaced without touching the API.

Workspace dependency groups
diagram

Rendering diagram

flowchart TB
  api["apps/api-worker"]
  libs["apps/agent-service · apps/execution-worker<br/>in-process libraries"]

  gov["Authority and policy<br/>mandates · policy · wallet-policy<br/>capability-registry · condition-engine · automations"]
  ident["Identity<br/>tenancy · portal · agent-record"]
  venues["Venue adapters<br/>adapter-morpho · aave · compound · suilend<br/>adapter-kyber · 0x · uniswap · lifi · jupiter<br/>adapter-bridge · broker-router · broker-cow"]
  infra["Infrastructure<br/>ledger · ledger-postgres · wallet-turnkey"]

  contracts["Shared contracts<br/>intent · execution-contract · verifiability"]

  api --> libs
  api --> gov
  api --> ident
  api --> venues
  api --> infra
  libs --> gov
  libs --> venues
  libs --> infra
  gov --> contracts
  venues --> contracts
  infra --> contracts
  ident --> contracts

Grouped rather than drawn per package. The api-worker imports from every group; groups depend on the shared contracts and never on the api-worker.

packageowns
intentAvaIntent and StrategyIR schemas, intent hashing, and decomposeWorkflow, which turns one sentence into dependent legs.
execution-contractFixed-point money, the execution state machine, and claim identity. One owner, consumed by every lane.
verifiabilityReceipt schema, RFC 8785 canonical hashing, Ed25519 receipt signing, proof and settlement standing, anchor payloads.
mandatesMandate store and hash, the enforcement gate, EIP-712 plan and standing signatures, revocation, standing consult and debit.
policyDeterministic risk policy: allowlists, notional and daily limits, expiry, and the human-confirmation threshold.
capability-registryEvery kind × chain × venue row with its implemented, apiReachable, mcpReachable, operatorEnabled and mainnetProven flags.
tenancyUsers, agent instances, wallet families and per-user Turnkey sub-organization ids.
ledger-postgresEvery durable store and its schema: receipts, claims, reservations, tokens, envelopes, revocations, anchors.
wallet-turnkeyTurnkey client, sub-organization provisioning, EVM and Sui signing.
adapter-*One venue each. Quote and build calldata from live state; never fabricate a route. Most stop at unsigned calldata.

Boot and runtime

apps/api-worker/src/runtime.ts builds the app from the environment. Postgres mode applies every schema before serving and hydrates the mandate cache, so the API never answers from a view that is missing mandates the database holds. Asking for Postgres without a DATABASE_URL is a boot error, not a fallback to memory.

Boot sequence
diagram

Rendering diagram

flowchart TB
  start(["index.ts → createRuntimeAppFromEnv"]) --> mode{"AVA_LEDGER_MODE"}
  mode -- "in_memory" --> mem["In-memory stores<br/>developer machine"]
  mode -- "postgres" --> url{"DATABASE_URL?"}
  url -- "missing" --> fail["Boot error, no fallback"]
  url -- "set" --> schema["Apply every schema idempotently<br/>then hydrate the mandate cache"]
  schema --> wire
  mem --> wire["createApp with Turnkey provisioner, CoW transport,<br/>API keys, auth mode, operator token, CORS"]
  wire --> serve["Serve on PORT, then start the scheduler<br/>only if AVA_SCHEDULER_ENABLED=true"]

The scheduler

lib/scheduler.ts runs inside the API process when AVA_SCHEDULER_ENABLED=true, which is why the Fly machine is never auto-stopped. Every 60 seconds it evaluates active automations and mandates against a live CoinGecko price and evaluates due standing envelopes. A separate, slower timer publishes receipt chain heads to Base when an anchor key is configured.

The scheduler never submits an order. Standing rotations spend only when an execute function is bound and the envelope has tickEnabled: true; index.ts binds no execute function, so in production the standing tick is evaluation only. Anchoring is the scheduler’s only gas-spending duty.

Infrastructure

Production is one always-on Fly.io machine in Singapore beside its managed Postgres, plus two Vercel projects. The /health check reports the ledger mode and dependency modes, and is what Fly polls every 30 seconds.

Production topology
diagram

Rendering diagram

flowchart TB
  dns(["www.getava.xyz"])

  subgraph vercel["Vercel"]
    direction LR
    siteapp["apps/site<br/>Next.js 16"]
    webapp["apps/web<br/>Vite console"]
  end

  subgraph fly["Fly.io · ava-v4-api · region sin"]
    direction LR
    machine["api-worker container<br/>shared-cpu-1x · 1 GB · min 1, never auto-stopped"]
    flypg[("Managed Postgres<br/>same region")]
  end

  subgraph ext["External"]
    direction LR
    tk["Turnkey API"]
    rpcs["Chain RPC endpoints"]
    apis["Venue and price APIs"]
  end

  dns --> siteapp
  siteapp -- "/console · /workspace · /assets" --> webapp
  siteapp -- "/v1 · /mcp · /health" --> machine
  machine --> flypg
  machine --> tk
  machine --> rpcs
  machine --> apis

From fly.toml, the Dockerfile and apps/site/next.config.ts. The API region was chosen to sit beside Postgres and avoid a cross-region hop on every ledger write.

The repository also carries a root vercel.json that serves the same Hono app as a Vercel function through api/, and a railway.toml for the Dockerfile. Both boot the same createRuntimeAppFromEnv. The README and the site’s proxy default name Fly.io as production; whether the other two are deployed anywhere today cannot be determined from the repository.

External integrations

serviceused foron the spend path
TurnkeyWallet provisioning in per-user sub-organizations; EVM and Sui transaction signing; the receipt anchor key.yes
Chain RPCPosition reads, nonce and fee estimation, broadcast, receipt polling, settlement event reads. Base supports a list of fallback URLs.yes
Kyber, 0x, LI.FI, Uniswap v3Swap quotes and calldata for workflow swap legs, gated by the registry's router allowlist.when a swap leg runs
AcrossMonad to Base USDC bridge deposits, with a destination fill reader.when enabled by the operator
CoW Protocol, JupiterCopilot swap quotes. CoW defaults to a dry-run transport unless AVA_COW_TRANSPORT=http.copilot only
CoinGeckoLive price snapshots for mandate and automation conditions and notional pricing.pricing input
DefiLlamaYield opportunities ranked by @ava/yield-router.no
OpenRouterOptional structured intent extraction in the copilot, behind a 2.5 s deadline.no
MembaseOptional external agent memory when AVA_MEMORY_URL is set.no

Configuration

Every setting is read from the environment at boot. The defaults are the safe ones: auth is required unless a developer opts out, operator controls are disabled without a token, and receipts are recorded as unsigned rather than signed with an absent key.

variableeffect
AVA_LEDGER_MODE · DATABASE_URLpostgres or in_memory. Postgres without a URL refuses to boot.
AVA_REQUIRE_AUTH · AVA_DEV_ALLOW_UNAUTHENTICATEDAuth is required unless the second is set on a developer machine. Configured API keys force it back on.
AVA_API_KEYSuserId:key bindings for headless callers. A malformed entry fails boot.
AVA_SIGNER_PROVIDER · TURNKEY_*turnkey enables real wallet provisioning and signing. Anything else is a boot error.
AVA_OPERATOR_TOKENEnables pause-all, resume-all, per-execution pause and revoke, and usage. Absent means those routes are disabled.
AVA_SCHEDULER_ENABLED · AVA_SCHEDULER_INTERVAL_MSTurns the in-process tick on. Not a spend switch.
AVA_RECEIPT_SIGNING_KEY · _KEY_IDEd25519 receipt signing. Unset means receipts carry an unsigned reason.
AVA_RECEIPT_ANCHOR_*Chain, address, interval and Turnkey org for publishing receipt chain heads.
AVA_CONTROLLED_SIGNERSAddresses Ava controls. A plan or standing signature from one of them is refused, so Ava cannot authorise itself.
AVA_BASE_RPC_URL(S)Base RPC endpoints, tried in order.
AVA_CORS_ORIGINSComma-separated browser origins for direct API calls.
AVA_COW_TRANSPORThttp for live CoW; the default is a deterministic dry run.

Where to go next

  • Core workflows walks onboarding, a live lend, a multi-leg plan, standing authorization and verification as sequence diagrams.
  • Data and trust covers credentials, what is stored where, the receipt model, and the known gaps.
  • API and MCP reference lists the endpoints, the live MCP tool list, and which registry rows execute today.