x402USDC on Base + Solana

x402-transfer-market

A booking token is a signed, transferable claim on a held reservation. List one for a $0.005 fee, and a buyer pays the ask to receive the same token reassigned to their wallet — signed, in the 200 body. Holders live on either rail: an EVM address or a Solana pubkey.

git clone https://github.com/nirholas/x402-transfer-market && cd x402-transfer-market
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 transfer works

1 · MINTFree: a booking token held by an EVM address or Solana pubkey
2 · LIST$0.005 → signed listing with the ask and a payout wallet
3 · 402Buyer calls /buy/:id; server quotes the listing's ask on both rails
4 · 200Reassigned token + new holderKey + signed transfer receipt

The token keeps its tokenId across owners; what changes is holder, the bumped transferCount, the signature, and the holderKey. Only the current holder's key can list it again.

Pricing

RoutePriceReturns
POST /list$0.005Signed listing of a transferable booking token
POST /buy/:listingId$0.750 (the listing's own ask)Booking token reassigned to the buyer wallet, signed
POST /bookingsfreeSigned booking token + its secret holderKey
GET /listingsfreeOpen signed listings
GET /listings/:idfreeSigned listing
GET /holdings/:walletfreeSigned booking tokens (holderKey redacted)
GET /transfers/:idfreeSigned transfer receipt
GET /statsfreeAggregate market counters
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));

// free: mint a booking token held by an EVM wallet
const token = await (await fetch("http://localhost:4023/bookings", {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ holder: "0xSellerAddress",
    booking: { kind: "table", venue: "Osteria Fiorentina", party: 2 } })
})).json();

// $0.005: list it, payout to a Solana wallet
const listing = await (await payFetch("http://localhost:4023/list", {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ tokenId: token.tokenId, holderKey: token.holderKey,
    ask: "$0.75", payoutTo: "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW" })
})).json();

// ask price: buy it — the reassigned token comes back in the response
const bought = await (await payFetch(`http://localhost:4023/buy/${listing.listingId}`, {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ buyer: "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW" })
})).json();

bought.token.holder;      // { rail: "solana", address: "Wwwu…T3WwW" }
bought.token.holderKey;   // fresh secret — only the buyer sees it

Discovery