Freeze, authorize, settle, execute
Both materialize and read use the same handshake:
- Request the unpaid work with its scope and window.
- Read x402
PaymentRequiredfromstructuredContentorcontent[0]and the quote{quote_id, amount, lines}fromcontent[1]of the MCP error result. - Confirm that spending is authorized for this scope and amount. A configured Testril CLI treats its operator-set budgets as that authorization and pays qualifying quotes without another prompt. Otherwise present unresolved choices to the operator. Mock payments follow the same consent workflow.
- Retry the same tool and arguments with the signed x402 PaymentPayload in
_meta["x402/payment"]. - Save
_meta["x402/payment-response"]with the result.
A quote is not the result; a settlement receipt is not the result; an accepted job is not necessarily completed data.
The quote clock
Every quote expires 120 seconds after freeze, whether paid or unpaid. Pay and start execution within that interval. Repeating a pending request does not extend the clock. After expiry, the same unpaid call freezes a new quote. A lost paid answer retried with the same spent payment or the same x402 payment-identifier returns the stored purchase without a second charge, including after the original quote expires. Reusing an identifier for different work is refused as payment_identifier_conflict. An already-started paid background job continues after quote expiry.
Amounts and rails are discovered
A deployment can offer several payment networks. Select one complete
accepts row and keep its network, asset, receiver, and channel state together
through signing and retries. Do not mix fields from different offers. A client
using the Testril CLI can select an advertised network with
--network eip155:<id>. This is separate from the computation chain.
Use inspect subject:pricing for published meters and the actual quote for this operation's price. Do not hardcode rates, payment networks, contracts, or pay-to addresses from an example. Chain of computation and payment network are separate choices.
Testril 0.9.0 represents every ordinary money field on the MCP and REST wire as an object with two strings:
{"atoms":"23","usd":"$0.000023"}
Use atoms for exact arithmetic and signing decisions. It is the base-10
integer count of USDC atoms (1 USDC = 1,000,000 atoms). Present usd exactly as
the server supplies it; it is display text, including the $. This shape
applies to payment-required and quote amount, quote lines[].amount and
lines[].unit_price, native quote est_spend, materialize receipt amount,
materialize and read replay amount and nested receipt.amount, job spent,
stop job.refunded, pricing rates[].unit_price, and ask retention
price_per_row and spend.
Two fields deliberately use strings instead of money objects:
- x402
accepts[].amountis the bare decimal string of atom units required by x402. It settles the exact quoted amount; compare it with the quote amount'satoms. - NFT accounting amounts remain bare decimal strings of USDC atoms. See data liquidity and rewards.
An illustrative quote fragment shows the boundaries:
{
"amount": {"atoms":"23","usd":"$0.000023"},
"lines": [{
"label":"query_blocks",
"units":13,
"unit_price":{"atoms":"1","usd":"$0.000001"},
"amount":{"atoms":"13","usd":"$0.000013"}
}],
"accepts": [{
"scheme":"batch-settlement",
"amount":"23",
"extra":{"minDeposit":"10000"}
}]
}
For example, exact-value code should cross-check atom strings rather than parse the dollar display:
quote_atoms = int(quote["amount"]["atoms"])
assert int(selected_accepts["amount"]) == quote_atoms
display_price = quote["amount"]["usd"]
The payment is a signed x402 v2 object attached by the MCP host in
_meta["x402/payment"]. It is not a tool argument, private key, or
caller-invented amount. Follow the server instructions and selected accepts
row. A model normally cannot set _meta; a payment-capable host or CLI does.
Enforce spending caps in the agent's wallet or signer, outside Testril tool
arguments and quote objects.
Testril CLI relay
The published CLI can run as a local MCP relay with
npx -y testril@latest mcp. Give the payer key to that process through
TESTRIL_PAYER_KEY, never as a tool argument. Configure
--per-tx-budget-usd and, when needed, the daily, weekly, monthly, minimum
deposit, and advertised-network options before starting it.
Those budgets are the operator's go-ahead: the relay pays a qualifying quote
without an extra dialogue, refuses an amount outside them before signing, and
returns an outcome: "error" such as payment_over_budget with a remedy.
After a paid call, report the charge note and retain
_meta["x402/payment-response"]; do not describe a small read as free merely
because it used an existing channel balance.
Batch settlement
For an offered batch rail, inspect subject: "channels" with the payer address and optional advertised CAIP-2 network. Reuse eligible open channel state according to the server's instructions. A first deposit and a later voucher are different actions. The declared and server-enforced minimum deposit is 10,000 atoms ($0.01), whatever the call's price; an offer carries it as the bare atom string "10000" in accepts[].extra.minDeposit. Obtain authorization for the full deposit commitment, not just the small individual query price.
Channel fields such as balances and cumulative amounts are decimal strings. Keep precise integer arithmetic. Withdrawal is a separate on-chain escrow workflow; Testril has no MCP withdrawal tool. Use the delay and contract details advertised by the live service.
Materialize pricing and retries
Materialize buys unpaid holes, excluding durable coverage and open paid ranges. extra_cache_days > 0 adds retention beyond the included stamp. Read has its own query pricing; cached computation does not imply free reads.
For a Function that carries state across blocks, materialization is quoted, billed, and walked from its contract's creation block. Read the actual frozen window and line items; a later requested start does not remove that prerequisite work.
Paid materialization starts an asynchronous job. Inspect the returned job until done or failed, and reconcile its error and refund records before retrying. Do not blanket-repay every error or assume a settlement receipt means the data is complete.
Payment recovery
If settlement may have succeeded but the purchase could not be recorded, the
tool returns outcome: "error" with
kind: "payment_recovery_pending". Its payment names the quote, network,
amount, settlement status, and any known settlement reference. The daemon
retains the original signed payment and retries completion, including after
restart.
Do not sign a replacement payment. Retry the original paid call to retrieve the purchase. Quote expiry and a missing receipt do not prove that settlement failed; an unknown outcome remains pending until finalized chain evidence resolves it.
Agent expense record
Log quote scope, lines, amount, rail, time frozen, settlement response, job ID, and final coverage. Log signed payloads only under the operator's security policy; avoid leaking keys or reusable authorizations into ordinary diagnostic output.