Skip to Content
How it worksThe services

The services

Four off-chain services keep the pool moving. One operator runs all four in one process, the node. None of them holds a note secret, and none of them is trusted by the agent: the chain checks every transaction they send, and the SDK checks every answer they give against the chain.

The zx402 system: the agent SDK, the one-process node, any x402 seller, and the pool on CardanoThe zx402 system: the agent SDK, the one-process node, any x402 seller, and the pool on Cardano
Who runs what, and what crosses each boundary. Source: docs/diagrams/system.excalidraw.

The indexer

The indexer follows the pool from its first transaction and rebuilds the state off chain: every leaf of the state tree, every label, every spent nullifier hash and every deposit output. After each pool transaction it checks its rebuilt tree root and nullifier root against the new pool datum. If they differ it stops with an error rather than serve wrong data.

It serves over HTTP, on port 4010 by default.

RouteReturns
GET /v1/poolThe pool state: roots, size, queue, nullifier root, fees, the ASP root, the config and the chain tip
GET /v1/leaves?from=&limit=A page of state tree leaves, with the root and size they belong to
GET /v1/asp/leaves?from=&limit=A page of approved labels
GET /v1/deposits?from=&limit=Deposit outputs, absorbed or waiting, with their precommitment and refund key
GET /v1/nullifiers?from=&limit=Spent nullifier hashes

The SDK and the crank never trust a page on its own. They rebuild the tree from the leaves and compare the root with the pool datum that they read from the chain themselves. A lying or stale indexer produces a mismatch, and the client refuses to go on.

The crank

The crank turns waiting deposits and queued change notes into tree leaves. On each tick it reads the pool, the config and the deposit outputs from the chain, downloads the leaves from the indexer, rebuilds the tree and checks the root. Then it plans an Insert: up to 4 slots, queued notes first, then deposits that pass the config limits. It makes the insert proof, builds the transaction, evaluates it, signs it with the crank key and submits it.

The crank remembers the pool output it spent. While that spend waits for a block it does nothing, and after 3 minutes without a confirmation it tries again. In a token pool, it keeps the deposit output’s minimum ADA, about 1.4 ADA, and takes no crank fee in tokens. The note receives v = gross - fee, where fee is the protocol deposit fee, zero in M0. In an ADA pool, it takes the configured crank fee, 0.3 ADA per deposit. Note inserts earn nothing, and the relayer fee is meant to cover them.

The ASP service

The ASP service approves deposits. It watches the labels that the indexer reports, applies its policy, and keeps the approved labels in a file. When new labels pass, it builds a Merkle tree of the approved set and updates the ASP output with the new root, signed by the operator keys. The demo policy approves every deposit. An operator can pass a deny rule.

The approved tree uses the same Poseidon hash and depth as the state tree, so one spend proof can prove membership in both.

The relayer

The relayer submits Settle transactions for agents. It serves over HTTP on port 4011 by default.

RouteWhat it does
POST /v1/quoteTakes the payouts. Returns withdrawn, protocolFee, total relayerFee, payoutLovelace, relayerKeyHash and the validUntil deadline
POST /v1/settleTakes the proof, the public inputs and the intent. Checks the proof, builds and submits the Settle transaction. Returns an ID
GET /v1/settle/:idReturns submitted or confirmed with the transaction hash
GET /v1/poolThe same pool view as the indexer

The quote’s payoutLovelace is the ADA the relayer attaches to every token payout, expressed in lovelace. It defaults to 2,000,000 (2 ADA) in a token pool and is zero in an ADA pool. The relayer’s own wallet supplies that ADA and the transaction fee. Each output receives exactly its requested tokens plus that ADA. The payout floor is 1 token base unit in a token pool and 1,000,000 lovelace (1 ADA) in an ADA pool.

Before it submits, the relayer checks the proof against the spend verification key, checks that the state root is one of the 16 in the datum, that the ASP root is current, that the nullifier is unspent, that the queue has room, and that the quote has not expired. Each failure has its own error code.

CodeMeaning
bad_requestThe body is malformed
invalid_proofThe proof does not verify
intent_mismatchThe intent does not match the quote or the context
stale_rootThe state root is no longer in the history
stale_asp_rootThe ASP root changed. Prove again with the new one
nullifier_spentThe note was already spent
queue_fullThe pool queue holds 8 entries. Wait for the crank
quote_expiredThe deadline passed
not_foundUnknown settle ID
internalAny other failure before a transaction was sent
uncertainThe relayer sent a transaction and cannot say whether it will land

The uncertain code matters for money. Once the relayer has called submit, it never reports a definite failure, because the transaction may still land. The SDK keeps its note reserved after an uncertain answer, polls the settle ID, and lets the chain decide. Only the ten definite refusals release a note for a new attempt.

In a token pool, the relayer charges its configured relayerFee * payoutCount, which defaults to 1 tUSDM per payout. Each payout costs it payoutLovelace of its own ADA, so two payouts earn 2 tUSDM and require 4 ADA before the network fee. The quote’s relayerFee contains the total charge. The withdrawn amount is the sum of the payouts, the protocol fee and that charge. An ADA pool charges once per quote, 1 ADA by default. The agent fixes the withdrawn amount inside the proof, so a relayer cannot raise the fee after the fact.

The node

npm run node -w ops starts all four services with one Blockfrost project ID and one operator seed from .env. The crank, relayer and ASP keys derive from that seed with different role paths, so one funded operator wallet feeds three roles. The node writes the ASP store to disk and logs each transaction.

The node is an operator’s tool. It can run anywhere. Several operators could run independent nodes against one pool, because nothing in the pool trusts them. Only the ASP keys are special, and only for approvals.

What the SDK checks

The services are convenient, not trusted. The SDK trusts its own seed and its own chain provider, and nothing else.

  • It reads the pool datum, the config and the chain tip with its own provider.
  • It rebuilds the state tree and the ASP tree from the indexer’s leaves and compares the roots.
  • It reads the fee rate from the config output on chain, not from the quote.
  • It refuses a quote whose deadline is past or more than 15 minutes after the tip.
  • It refuses a total relayer fee above its own limit, 2,000,000 pool asset base units by default (2 tUSDM or 2 ADA).
  • For token x402 payments, it refuses payout ADA below the seller output’s minimum plus the second transaction’s fee, about 1.4 ADA.
  • It follows the exact output of a deposit it sent itself, so a copied precommitment cannot take its place.

The SDK page shows how an agent uses all this.