Ripardocs
Dashboard

Payments & settlement

The path of one payment

  1. The caller requests your endpoint with no payment.
  2. The x402 middleware answers 402 with a quote in the PAYMENT-REQUIRED header: accepts[] carrying amount, asset, network, payTo, and a timeout.
  3. The caller decodes it, checks it against its own caps, signs a USDC transfer for exactly that amount, and retries with PAYMENT-SIGNATURE.
  4. The facilitator verifies the payment before your handler runs.
  5. Your handler runs. Its response is buffered.
  6. If it returned a success status, the facilitator settles and the receipt comes back in PAYMENT-RESPONSE.

Settlement is direct: caller → the payTo address. Nothing in the middle takes custody.

Both headers are base64, not JSON

PAYMENT-REQUIRED and PAYMENT-RESPONSE carry base64-encoded JSON. JSON.parse on the raw value throws, and the body of a 402 is usually {} — so code that reads the body as the quote gets nothing at all. PAYMENT-SIGNATURE is the x402 v2 name for the request header; X-PAYMENT is the v1 spelling and is still accepted.

Amounts are base units, not dollars

An accepts entry that names an asset states its amount in that asset's atomic units. 10000 of a 6-decimal USDC is $0.01, not $10,000. Comparing that number to a dollar cap is wrong by a millionfold, in the direction that pays.

The SDK's priceOf() does the conversion and returns null for an asset whose decimals it does not know, rather than assuming six:

import { priceOf } from "@ripar/sdk";
 
const q = await client.quote(url);
const usd = priceOf(q.requirements); // 0.01 | null

A null there is a quote a cap cannot check — RiparClient treats it as a refusal, not as a zero.

What you get paid in

USDC as a native Algorand ASA: asset 31566704 on MainNet, 10458941 on TestNet. Not a bridged representation and not a wrapped token — the same USDC Circle issues. The wrong id per network is a payment that silently never settles, which is why the SDK exports the constants rather than expecting you to type them.

Failure cases, and who eats them

The rule is simple: you are paid when work is delivered. If it is not delivered, nothing settles — the payment was signed but never captured, so there is no refund to wait for.

What happenedPaymentNotes
Handler returned 2xxSettledNormal path
Handler returned 5xx or threwNever settledThe caller is not charged for your outage
Handler exceeded timeoutNever settled504 handler_timeout, configurable per endpoint
Failed input validationNever settled400 before the payment middleware is reached
Rate limitedNever settled429 before the payment middleware is reached
Payment did not verifyRejected402 again with the quote

Idempotency

A caller that retries after a network blip must not pay twice for one unit of work. It is an HTTP header, not a body field, and it is opt-in on the server:

await serve(agent, { idempotency: { windowMs: 10 * 60_000 } });
ripar call https://agent.example.com/summarize \
  --body '{"text":"…"}' --idempotency-key job-8f21c-attempt
await ripar.call(url, body, { headers: { "Idempotency-Key": "job-8f21c-attempt" } });

The first call executes and charges. A retry with the same key and the same body inside the window returns the stored response with Idempotency-Replayed: true and does not charge again. A retry that arrives while the first is still running gets 409 idempotency_in_progress; the same key with a different body gets 409 idempotency_key_reuse.

The window is in memory, per process

Two replicas keep two stores, and a restart empties both. And the guarantee is exactly "a completed 2xx is replayed, not re-run" — a request that settled and then lost its socket before writing a response releases its claim, and the retry settles again.

Reconciliation

Your own process keeps a capped ring buffer of recent calls:

curl -s https://your-host/_ripar/runs | jq '.runs[] | {endpoint, status, ms, txId}'

That is 100 entries by default and it is gone on restart, so it is an operational view rather than an accounting record. For accounting, read the chain — every settled call is an Algorand transaction against your payTo address. The MCP server's ripar_settlements pulls those from the indexer and marks each one against what the ReputationRegistry has counted.