Skip to content

How it works

PipRail’s whole design follows from one constraint: no backend. No server PipRail runs sits between you and your money. That sounds like a limitation; it’s the opposite. It’s what lets payments settle straight to your wallet, with nothing to sign up for and no fee to skim.

A payment is a four-step HTTP exchange:

  1. Challenge. The client requests a gated resource. The server answers 402 Payment Required with an x402 challenge describing what it will accept (chain, asset, amount, recipient, a one-time nonce).
  2. Pay. The client picks an acceptable rail, broadcasts the transfer on-chain from its own wallet, and re-requests the resource, this time carrying a proof of payment.
  3. Verify. The server re-derives every field it cares about from its own trusted challenge (never the client’s echo), then checks the proof on-chain against its own RPC.
  4. Deliver. If the proof is valid, recent, and unused, the server returns 200 with the goods.
client ──GET /report──────────────▶ server
client ◀──402 + challenge────────── server
client ──(pay on-chain)──▶ blockchain
client ──GET /report + proof──────▶ server ──(verify on-chain)──▶ its own RPC
client ◀──200 + the goods────────── server

Every proof that reaches the gate produces a verdict, and both parties learn it with the same reason:

  • It settled. The buyer gets the 200 plus the receipt (and a payment-settled event); the merchant’s onPaid hook fires with the verified PaidReceipt.
  • It was rejected. When verify() returns kind: 'invalid' (wrong amount, expired, replayed, wrong recipient, bad signature) the buyer’s client throws (and emits a payment-failed event), and the merchant’s onFailed hook fires with a FailedPayment. The machine code on each side is the same, so a log on one matches a log on the other.

A no-proof first request (the normal 402 challenge) is neither. It fires no hook, because it isn’t a verdict.

Verification is local, and there’s no facilitator

Section titled “Verification is local, and there’s no facilitator”

A “facilitator” is the third party most x402 stacks use to verify and settle payments. PipRail has none. The server verifies the proof itself, on-chain, against the RPC you give it, plus an in-memory used-proof set and a recency window to stop replays.

Proof binding, so a payment can’t be forged or replayed

Section titled “Proof binding, so a payment can’t be forged or replayed”

A proof must be cryptographically bound to the specific challenge it answers. PipRail uses two templates, picked per family:

  • Template A, memo/nonce-bound (Stellar, XRPL, NEAR, Algorand, TON): the challenge nonce rides in a memo/note/comment, and verify() matches it on the merchant’s own account.
  • Template B, digest-bound (EVM, Solana, Tron, Sui, Aptos, and every native coin): the proof is the transaction hash/digest, verified by reading the transaction plus a recency window and a single-use proof set.

Either way, verify() re-derives every checked field from the trusted challenge, so a forged echo can’t redirect it. The details per family live in Proof binding.

Under the hood, each chain family is a self-contained driver (evm, solana, ton, …) implementing one contract. The protocol layer, meaning the parts that issue challenges, parse envelopes and run the client, depends only on that contract, never on viem or any chain library. That’s what makes non-EVM families lazy-load on demand: a pure-EVM install never pulls in Solana or TON code.

Read more in The PaymentDriver architecture.

The x402 v2 spec (§7) explicitly permits merchant-local verification: “resource servers MAY… host the endpoints themselves.” So PipRail’s shape isn’t a clever workaround; it’s the spec-blessed path, just taken all the way.