x402-campsites

Campground search and live site-level availability from Recreation.gov. Which individual sites are free, on which nights — one paid HTTP call, answer in the response body. Pay in USDC on Base or Solana — your client picks the rail.

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

Pricing

RoutePriceYou get back
GET /search$0.002Campgrounds by text, state, or coordinates — location, site count, amenities, booking link
GET /availability/:campgroundId$0.003Site-level availability over a date window, plus a summary of what is bookable
GET /healthfreeBoth data sources and their live status
GET /catalogfreeReal recreation.gov campground ids — skip the paid search when you already know the site
GET /.well-known/x402freeMachine-readable price sheet, both rails

Search then check one campground is $0.005. If you already have the campground id from the free catalog, it is just the $0.003 availability lookup.

Two rails, one challenge

Every 402 lists both networks. A caller holding USDC on Base pays on Base; a caller holding USDC on Solana pays on Solana. Same route, same price, same artifact.

EVM

USDC on Base

base-sepolia by default, base for mainnet. EIP-3009 transfer authorization.

0x40252CFDF8B20Ed757D61ff157719F33Ec332402
SVM

USDC on Solana

solana mainnet by default, solana-devnet for testing. SPL transfer.

WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW

How a query flows

402quote in USDC on
Base and Solana
signclient picks a rail
and signs
retrysame request +
X-PAYMENT
200sites in body,
receipt in header

Quickstart

# 1. Run the service (works immediately — availability needs no key)
cp .env.example .env
npm run dev                # http://localhost:4025

# 2. Free: real campground ids
curl -s http://localhost:4025/catalog | jq '.campgrounds[0]'

# 3. See the dual-rail 402 quote (no wallet needed)
curl -s "http://localhost:4025/search?state=CA" \
  | jq '.accepts[] | {network, payTo, maxAmountRequired}'

# 4. Pay for real on Base Sepolia (free test USDC: faucet.circle.com)
export PRIVATE_KEY=0x…
npm run client             # searches, then checks live site availability

Two upstreams, two honesty rules

RouteUpstreamKeyFallback
/searchRIDB — the official Recreation Information DatabaseFree RIDB_API_KEYFixture catalog, labelled source: "fixture"
/availability/:idrecreation.gov availability APINone — keylessOnly if that endpoint is unreachable, and availabilitySource says so

Availability — the part that actually matters — is live out of the box, because the endpoint behind recreation.gov's own booking calendar is public. And the fixture catalog carries real recreation.gov facility ids, so an availability lookup on a fixture search result still returns genuine site-level data.

Read availabilitySource, not source. "recreation.gov" is real; "fixture" means the live endpoint was unreachable and the numbers are a deterministic simulation that must never be presented as bookable. The two fields are independent — a response can correctly read "source": "fixture" with "availabilitySource": "recreation.gov".

This service reports availability; it does not reserve anything. Every response carries a bookingUrl for a human to finish on recreation.gov.