The two legs
Both legs are USDC on Algorand Mainnet (ASA
31566704), and both transaction ids are returned in the report: money.seloInboundTxId and money.downstreamTxIds. Anyone can look them up on an explorer at https://allo.info/tx/<txId>.
Incoming before outgoing
Selo’s paid route does not rely on middleware ordering. It composes each step explicitly:1
Validate before any payment
Selo validates the request body and the
Idempotency-Key header, then checks that the preflight exists, is eligible and unexpired, that its authorization is verified, and that the target’s price is within the job budget. Any failure here answers with an error and nothing is charged.2
Verify the payment
The facilitator verifies your x402 payment. A request without a payment gets Selo’s
402 challenge.3
Claim the job
Selo inserts the job row under an idempotency key before it settles anything, so a duplicate request can never settle twice.
4
Settle and record
Selo settles your payment through the facilitator and records the settlement transaction id, payer, amount and time on the job. The inbound transaction id is unique: one settlement can back only one job.
5
Run
The runner starts only a job whose status is
INBOUND_SETTLED and whose settlement time is recorded. It refuses any other job and contacts no target.A12: inbound settles before the first downstream request checks the recorded timestamps and the call order against a real database.
Inbound outcomes
The idempotent claim
The job’s idempotency key is derived from your request:- With an
Idempotency-Keyheader (8 to 128 characters ofA-Z,a-z,0-9,_or-), the key is a SHA-256 hash of the preflight id and your header value. The same header with two different payments settles once. - Without the header, the key is a SHA-256 hash of the canonical JSON of your payment payload, so only a resend of the same payment payload counts as a duplicate.
The
selo CLI sends a stable Idempotency-Key for each run, which is what makes recovering an interrupted run safe.
The downstream leg
Selo signs at most one downstream payment per job. The replay inretry_safety reuses that same signed payment; it is not a second payment.
- Read the price.
handshakeselects yourexactrequirement for Algorand Mainnet USDC. - Check the wallet. Selo confirms its operating wallet can cover the price. If not, nothing is reserved or signed.
- Reserve. The spend guard and one conditional database update reserve the price. See the budget invariant.
- Sign exactly what was reserved. The x402 client signs only an entry whose network, asset, amount and
payTomatch the reservation, with its spend cap set to that amount. Selo records the id of the transfer it signed and the transfer’s last valid round. - Send once and classify. The outcome moves the payment to
SETTLED,RELEASEDorUNRESOLVED, as described below.
Spend limits
The budget invariant
Before every downstream signature, this must hold:Invariant
settled, reserved and unresolved are counters on the job row, in micro-USDC. Selo enforces the invariant in one database transaction:
- It evaluates the spend policy on the current job and authorization. The first failing rule becomes the denial reason.
-
It claims capacity with one conditional
UPDATE. There is no read-then-check-then-write in application code:reserve.ts (shape of the generated SQL) -
Zero rows returned means the reservation is denied. Selo re-reads the job to name the reason:
JOB_NOT_RUNNING,PAYMENT_UNRESOLVEDorJOB_BUDGET_EXCEEDED. -
In the same transaction, it inserts the
downstream_paymentsrow with statusRESERVED.
settled + reserved + unresolved <= max_spend, max_spend <= 5000000, every counter non-negative, and every payment amount positive. A settlement transaction id is unique across all downstream payments.
The acceptance test
A16 fires truly parallel reservations at a real PostgreSQL database: three parallel 0.05 USDC reservations against a 0.10 USDC job reserve exactly two, and thirty parallel mixed-size reservations against 0.50 USDC never overspend.Spend denial reasons
The spend guard evaluates its rules in this order and reports the first that fails. A denied payment is never signed;paid_delivery reports SPEND_GUARD_DENIED and the run records the inconclusive reason shown.
Payment states
A downstream payment has four states.
Each transition is a conditional
UPDATE ... WHERE status = <from> on the payment row plus the matching counter move on the job, in one transaction. A transition that finds the payment already moved changes nothing.
What moves a payment
Why ambiguity stops spending
Once a signed payment has left Selo, a missing answer does not mean the money stayed put. A timeout can hide a settlement that lands a second later, and a signed transfer can be submitted by whoever holds it until its last valid round passes. If Selo released the capacity on a timeout and paid again, it could pay twice. So Selo treats every unclear outcome the same way:- The amount moves to
unresolvedand keeps counting against the budget. - The job stops spending: the spend guard refuses any further reservation while
unresolvedis above zero, andretry_safetydoes not replay. - The run records
PAYMENT_UNRESOLVED, and the verdict is INCONCLUSIVE unless a check already proved a FAIL. - Only a definite failure before the payment left Selo releases capacity. A timeout never does.
Money is integer micro-USDC
Inside Selo, every amount is an integer number of micro-USDC (USDC has 6 decimals). Floating point is never used for money. Decimal strings exist only on the wire and in output, and x402amount fields use atomic units.
Selo parses a decimal only when it has no sign or exponent and at most six fractional digits. It formats with at least two decimals and drops trailing zeros beyond that, so 1,234,500 micro-USDC prints as
"1.2345".
Operation ids and Idempotency-Key
When a job starts, Selo plans one scenario per check. Each scenario gets an immutable operation id,<jobId>:<checkId>, unique within the job and enforced by a database unique index. A retry of the same logical operation reuses its id.
When your challenge declares the x402 payment-identifier extension, Selo also sends a payment id derived from the operation id:
selo_ followed by the operation id with every character outside A-Z, a-z, 0-9, _ and - replaced by _, padded or cut to the extension’s length limits. The report lists each scenario’s operation id and attempt count.
Job states
Every job transition is a conditional update from the expected state, so two processes can never move the same job twice. A job whose inbound settlement is unconfirmed stops at
INCONCLUSIVE and never gets a report.
Scenario states
Related
Held payments and reconciliation
How an UNRESOLVED payment is settled or released on chain proof.
Pricing and limits
Every price, cap, timeout and size limit in one place.
Recover an interrupted run
Resume a run whose inbound payment outcome is unconfirmed, without paying twice.
Pay with any x402 client
Pay Selo’s challenge without the CLI.