x402USDC on Base + Solana

x402-disputes

Open a dispute against a payment that actually settled. Evidence is hashed, never stored — you get a signed case record with every hash and a combined root, a signed status snapshot per poll, and a signed ruling with a refund instruction naming the payee wallet on its rail.

git clone https://github.com/nirholas/x402-disputes && cd x402-disputes
npm install && npm run dev

Pay on either rail

Every paid route answers with a dual-rail 402. The accepts array carries one EVM entry and one Solana entry; your client (or the checkout modal) picks whichever wallet it has. Each rail settles through its own facilitator — no single one handles both.

EVM · BASEUSDC on base-sepolia / base
0x40252CFDF8B20Ed757D61ff157719F33Ec332402
settled via x402.org/facilitator
SOLANAUSDC on solana-devnet / solana
WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW
settled via facilitator.payai.network

How a dispute runs

1 · FILE$0.01 → signed case record naming the settled payment + evidence hashes
2 · WAIT72h response window; the case is flagged overdue after it
3 · POLL$0.001 per read → fresh signed status snapshot with a timeline
4 · RULEArbiter (free, authenticated) → signed ruling + refund instruction

Because only hashes are stored, anyone can later prove a document was the one filed: re-hash it at POST /evidence/hash and compare against the case's evidenceRoot. The service cannot leak evidence it never had.

Pricing

RoutePriceReturns
POST /cases$0.01Signed case record + evidence hash
GET /cases/:id$0.001 (per poll)Status snapshot
POST /rule/:idfreeSigned ruling + refund instruction
POST /evidence/hashfreeItem hashes, combined root, and a `matches` flag against a filed case
GET /casesfreeCase index
GET /rulings/:caseIdfreeSigned ruling
POST /verifyfree`{ valid: true | false }`
GET /healthfree`{ ok: true }`

Quickstart

import { wrapFetchWithPayment } from "x402-fetch";
import { privateKeyToAccount } from "viem/accounts";

const payFetch = wrapFetchWithPayment(fetch, privateKeyToAccount(process.env.PRIVATE_KEY));

// $0.01 — open the case. Evidence is hashed here and never stored.
const record = await (await payFetch("http://localhost:4025/cases", {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({
    claimant: "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW",
    respondent: { merchantId: "osteria-fiorentina.example" },
    disputedPayment: { network: "solana-devnet", transaction: "5Kq7xJ2mNsignature", amount: "$0.75" },
    claimType: "not_delivered",
    statement: "Arrived on time, no reservation on file.",
    evidence: [{ label: "booking-confirmation", content: confirmationEmail }]
  })
})).json();

record.evidenceRoot;   // fixes the whole bundle

// $0.001 per poll — each read is a fresh signed snapshot
const snap = await (await payFetch(
  `http://localhost:4025/cases/${record.caseId}`
)).json();

snap.status;   // "awaiting_response" | "ruled" | …
snap.ruling;   // signed ruling + refund instruction, once one exists

Discovery