Skip to main content
The demo merchant is a small x402 endpoint in the Selo repository, at examples/merchant. It sells one demo order for 0.01 USDC on Algorand behind the stock @x402/hono payment middleware, and it ships two releases at the same URL. Both are healthy and both settle payments. Only one keeps its promise to paying clients, and only a paid request against the release contract tells them apart.

The two releases

broken-v1 models a serializer change that renamed one field. The endpoint is up, the payment settles and the status is 200, yet every client that reads receiptId breaks.
Both ids are derived from the SHA-256 of the signed payment, so one payment always yields the same order and receipt. When Selo replays the identical payment, the facilitator refuses to settle it twice, and the merchant answers the replay with 402. The release contract in examples/merchant/contract.json requires orderId, receiptId and fulfilled: true:
contract.json
The request body Selo sends is examples/merchant/order.json: { "sku": "demo-widget", "quantity": 1 }. The merchant accepts a sku of 1 to 40 lowercase letters, digits or dashes, and a quantity from 1 to 10.

Prerequisites

  • A checkout of the Selo repository, Node 24 and pnpm 10, with pnpm install --frozen-lockfile.
  • For a real release test: a server with a public HTTPS domain, and a payee address on Algorand Mainnet opted in to USDC (ASA 31566704). The merchant only receives payments, so it needs the address, never a key.
  • For the paid runs: a payer wallet for selo test holding 1.00 USDC per run. See Run a release test.

Run it locally

Locally, the merchant is useful to see its 402 and its behaviour. Selo cannot test it there, because Selo only calls public HTTPS endpoints.
1

Start the merchant

Terminal
Output
2

Check its health

Terminal
Output
3

Ask for an order without paying

Terminal
The merchant answers 402 Payment Required with an empty JSON body and the challenge in the PAYMENT-REQUIRED header. Decoded, its requirement on Testnet is:
accepts[0]
10000 atomic units is 0.01 USDC. The challenge also declares a Bazaar extensions.bazaar block with the order input, an example receipt and the receipt schema. On algorand-mainnet, the network is algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8= and the asset is 31566704.
4

Run its tests

The merchant’s own tests run both releases against a fake facilitator, with no wallet and no network:
Terminal
They check that both releases pass an ordinary health check, answer an unpaid request with the same challenge, and settle; that broken-v1’s paid body fails the contract and fixed-v2’s matches it; that one payment yields one receipt; and that contract.json equals the receipt schema the merchant advertises.

Configuration

The environment is validated at startup, and the merchant refuses to start with a missing or invalid value.

Deploy it on your own domain

1

Prepare a payee address

Use an Algorand Mainnet address you control, opted in to USDC (ASA 31566704). Each paid release test pays it 0.01 USDC from Selo’s wallet.
2

Start the broken release on Mainnet

On your server, from the checkout:
Terminal
Keep it running under your process manager, and put it behind a reverse proxy that terminates TLS for your domain, for example nginx with a Let’s Encrypt certificate, forwarding to 127.0.0.1:8791. Set MERCHANT_PUBLIC_ORIGIN to exactly the origin clients use.
3

Check it from outside

Terminal
Expect {"status":"ok","release":"broken-v1"} and 402.
4

Describe it as yours

The 402 and the Bazaar listing carry the description in merchantDescription in examples/merchant/src/app.ts, which names Selo’s own demo. Edit it so your listing describes your deployment.

Reproduce FAIL, fix, PASS

Two paid runs cost 2.00 USDC from your payer wallet to Selo. Selo pays your merchant 0.01 USDC per run from its own wallet.
1

Authorize the route

Terminal
Write the printed selo-verification=… line to the file named by SELO_VERIFICATION_FILE. The merchant reads it per request, so no restart is needed. Then:
Terminal
2

Preflight for free

Terminal
Expect target fee 0.01 USDC, paid by Selo and you pay 1.00 USDC to Selo for the release test.
3

Test the broken release

Terminal
Expect response_contract FAIL SCHEMA_MISMATCH, then gate BLOCK (exit 1): FAIL: response_contract, and exit 1.
4

Ship the fix

Restart the merchant with MERCHANT_RELEASE=fixed-v2, then check:
Terminal
Output
5

Test the fixed release

Run the same selo test command with --release fixed-v2, --summary fixed-v2.json and --attestation fixed-v2-attestation.json. Expect every check to pass, then gate ALLOW (exit 0): PASS on every release check, and exit 0.
6

Verify both verdicts offline

Terminal
The first exits 1 with verdict FAIL, the second 0 with verdict PASS. Presenting the fixed-v2 attestation for broken-v1 exits 2. See Verify a signed verdict.
The discovery check compares your live 402 with your route’s Bazaar listing. On a fresh deployment the route may not be listed yet, and discovery_contract reports WARN NOT_CATALOGED: a warning that does not block a PASS. If Selo cannot read the catalog at all, the check is INCONCLUSIVE CATALOG_UNAVAILABLE, and so is the verdict, unless another check proved a FAIL. INCONCLUSIVE is never a PASS: retry later.

The recorded Mainnet runs

The public instance at https://demo.useselo.xyz/v1/orders is the one Selo tested on Algorand Mainnet on 26 October 2026, through the same CLI commands. Its health check answered 200 on both releases. You cannot run a paid test against that instance yourself: an authorization needs a line served from its origin, which only its operator can publish. Deploy your own copy to reproduce the runs. See Proof on Algorand Mainnet for the full record.

Troubleshooting