2. Policy decides
Every agent request goes through one pure function, evaluatePolicy in @zyropay/shared, inside a
database transaction that holds a row lock on the agent’s policy. It returns one of three decisions
and every reason that applied: deny, require approval (ask you), or allow (autonomous,
within limits). Read-only tools are allowed once the agent, tool, wallet and network checks pass;
approval applies to value only.
The rules, in order
Section titled “The rules, in order”- Who and where. Deny if the agent is revoked or its session expired, or if the tool, the wallet or the network is not in the agent’s allowed lists.
- What and to whom. For a value-moving tool, deny if spending is off; if the protocol is
missing or not allowed (
pay_x402must namex402); or if recipients are restricted and the recipient is not on the allowlist. For an x402 payment the recipient is the seller, also for an escrow payment where the money goes to a script first. With only pay registered Masumi agents on, deny a seller without an on-chain identity ZyroPay verified (AGENT_NOT_REGISTERED) or, if you keep a trusted list, one not on it (AGENT_NOT_TRUSTED). - How much. The request is valued in USD cents, rounded up. If the value plus what was already
spent and reserved today or this month exceeds a limit, or there is no valuation, the request
is over the limit. So is a direct (non-escrow) x402 payment above your escrow threshold
(
ESCROW_REQUIRED). Over the limit means deny or ask you, as you choose (overLimitBehavior). - Who signs. In Always Ask mode, for a swap, and for any Cardano payment, ask you. Otherwise allow: the payment runs autonomously within the limits.
A request that is allowed or sent to you for approval reserves its USD value at once; the reservation becomes a ledger entry when the chain confirms the payment, and is released if it is rejected, expires or fails.
Why it fails closed
Section titled “Why it fails closed”- No price, no autonomy. USD values come from Jupiter (Solana) and CoinGecko (Ethereum, Base, ADA). A quote older than 120 s is dropped. A missing valuation counts as exceeding every limit, so the request is denied or comes to you; it is never allowed.
- Test networks are valued at mainnet prices and labelled
mainnet_proxy, so limits mean the same thing on preprod as they would on mainnet. - A throw is a deny. Malformed amounts throw, and the caller treats a throw as a denial.
- Concurrency. Each agent’s requests are serialized by
SELECT … FOR UPDATEon its policy row, and limits count spent plus reserved. Ten requests fired at once cannot jointly pass a daily limit that one of them alone would fit. This is tested on an in-process Postgres and on real Postgres.
What a new agent may do
Section titled “What a new agent may do”Nothing that moves value. defaultAgentPolicy() is Always Ask, spending off, zero limits, recipients
restricted to an empty allowlist, no wallets, no protocols. Connecting the agent adds only the
wallets and networks you picked; everything else you switch on yourself in the policy editor. Every
field is in the policy reference, every reason code in
States and codes.
A worked example
Section titled “A worked example”The live x402 payment was 2 tADA from ChatGPT to the demo seller. For it to get past rules 1 to 3,
the agent’s policy had spending on, the x402 protocol allowed and the seller’s address on the
allowlist (the setup in the user guide). Rule 4 then asked
for approval, as it does for every Cardano payment, and the request came back PENDING_APPROVAL.
The same agent asking to pay an address that is not on the allowlist gets REJECTED with
RECIPIENT_NOT_ALLOWED, and nothing reaches the app or the chain (tested offline; no live denial is
recorded).
Proven
Section titled “Proven”- The rules, their order, the reasons and the fail-closed paths: unit tests in
packages/shared/src/policy.test.tsTested offline. - Concurrent requests against limits:
apps/api/test/security/concurrency.test.tsandapps/api/test/postgres/concurrency.test.tsTested offline. - The live payments on the Evidence page each passed this engine.
Code: packages/shared/src/policy.ts, packages/shared/src/domain/agents.ts,
apps/api/src/services/requests.ts.