x402 · USDC on Base + Solana

x402-grocery

Grocery search and priced carts an agent can buy one query at a time — real Kroger products, prices, and aisle locations.

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

Pricing

RoutePriceWhat you get back
GET /products$0.002matching products with regular and promo prices, size, aisle, and stock level
POST /cart$0.005a priced cart: matched line items, substitutions, line totals, subtotal, tax, and grand total
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 /products
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));

const res = await payFetch("http://localhost:4021/cart", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ items: [
    { query: "whole milk", quantity: 1 },
    { query: "eggs", quantity: 2 },
  ]}),
});
const cart = await res.json();   // priced cart, substitutions, totals

Live Kroger data is used when `KROGER_CLIENT_ID` and `KROGER_CLIENT_SECRET` are set (a free developer account at developer.kroger.com). Without them the service answers from a deterministic fixture catalog of ~30 common grocery items and labels every response `"source": "fixture"`. The shape of the response is identical either way, so an agent can develop against fixtures and switch to live data with no code change.

Links