x402 · USDC on Base + Solana

x402-shop-scout

Ask the secondhand market what something is worth — live eBay listings plus a priced verdict on whether a given listing is actually a deal.

git clone https://github.com/nirholas/x402-shop-scout
cd x402-shop-scout && npm install
npm run dev   # http://localhost:4022

Pricing

RoutePriceWhat you get back
GET /search$0.002normalized listings with price, shipping cost, condition, seller feedback, location and URL
GET /deal-check/:itemId$0.002a verdict (`great-deal` … `overpriced`), the comparable sample, median/mean total cost, delta, percentage, and plain-English reasoning
GET /healthfreeLiveness, data source, configured rails
GET /.well-known/x402freeMachine-readable discovery manifest

Every paid route returns the purchased artifact in the 200 body — nothing is deferred to a webhook or a second fetch.

How a paid call works

1 · RequestAgent calls GET /search
2 · 402Server quotes the price on both rails
3 · PayClient signs on its rail, retries with X-PAYMENT
4 · 200Artifact in the body + settlement receipt header

Two rails, one challenge

Pay in USDC on Base or Solana — your client picks. The 402 body always lists both.

EVM · Base

USDC on base-sepolia (or base). The wallet signs an EIP-3009 authorization locally — no server round-trip.

0x40252CFDF8B20Ed757D61ff157719F33Ec332402

Solana

USDC SPL transfer on solana. A facilitator sponsor pays the SOL network fee, so buyers need only USDC. Helpers at /api/x402-checkout build and wrap the transaction.

WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW

Quickstart for agents

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

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

// What's out there?
const { listings } = await (await payFetch(
  "http://localhost:4022/search?q=sony+wh-1000xm5&limit=5"
)).json();

// Is the cheapest one actually a deal?
const verdict = await (await payFetch(
  `http://localhost:4022/deal-check/${encodeURIComponent(listings[0].itemId)}`
)).json();

console.log(verdict.verdict, verdict.reasoning);   // "good-deal", "Item total cost ..."

Live eBay data is used when `EBAY_CLIENT_ID` + `EBAY_CLIENT_SECRET` (or a raw `EBAY_OAUTH_TOKEN`) are set — a free developer account at developer.ebay.com. Without them the service answers from a deterministic fixture marketplace of 15 listings across five product families and labels every response `"source": "fixture"`. The verdict logic is identical in both modes; only the listing corpus changes.

Links