Buidl Battle 2026 · Stacks Bitcoin L2 · Best x402 Integration
Travelers earn SIP-013 SFT stamps from verified eco providers. Stamps accumulate into tiered reputation (Bronze → Silver → Gold) unlocking sBTC rewards. Content is gated behind x402 USDC micro-payments on Base.
- Setup:
docs/SETUP.md - Testing:
docs/TESTING.md
- Architecture Overview
- Prerequisites
- Environment Setup
- Network Toggle Reference
- Testnet Setup Guide
- Running Locally
- Testnet Testing Guide
- Admin Panel
- Switching to Mainnet
- Contract Reference
- Troubleshooting
┌─────────────────────────────────────────────────────────┐
│ Browser (Next.js 14) │
│ • Stacks wallet: Leather / Xverse (ST.../SP...) │
│ • EVM wallet: MetaMask / Coinbase (0x...) [prod] │
└───────────┬──────────────────────────┬──────────────────┘
│ Stacks txs │ x402 USDC
▼ ▼
┌───────────────────────┐ ┌──────────────────────────┐
│ Stacks Testnet / │ │ Content Server :3001 │
│ Mainnet │ │ eco guides $0.001 │
│ │ │ carbon routes $0.002 │
│ provider-registry │ │ hotel list $0.003 │
│ stamp-registry │ │ listing fee $0.10 │
│ reward-pool │ └──────────┬───────────────┘
└───────────────────────┘ │ validates via
▼
┌──────────────────────────┐
│ Oracle Server :3002 │
│ secp256k1 booking proofs │
└──────────────────────────┘
Two independent network dimensions, both controlled entirely by .env:
| Dimension | Testnet | Mainnet |
|---|---|---|
| Stacks | STACKS_NETWORK=testnet |
STACKS_NETWORK=mainnet |
| EVM / x402 | X402_NETWORK=base-sepolia |
X402_NETWORK=base |
| Payments | DEMO_MODE=true (no wallet needed) |
DEMO_MODE=false (real USDC) |
| Tool | Version | Notes |
|---|---|---|
| Node.js | ≥ 18.0 | Required by all packages |
| npm | ≥ 9 | Comes with Node 18 |
| Leather wallet | Latest | Chrome extension — leather.io |
| MetaMask | Latest | Only needed when DEMO_MODE=false |
# 1. Clone and install all packages
git clone <repo> && cd ecostamp
npm run install:all
# 2. Create your .env from the template
cp .env.example .env
# 3. Generate an oracle signing key pair
npm run oracle:keygen
# Prints ORACLE_PRIVATE_KEY and signing-key-hash.
# Copy ORACLE_PRIVATE_KEY into .env.
# Save the signing-key-hash — you paste it when approving providers in the Admin Panel.These are the only vars you change to flip between testnet and mainnet. Everything downstream (USDC contract addresses, API URLs, chain IDs, Leather network resolution, viem chain selection) reads from these.
STACKS_NETWORK=testnet
STACKS_API_URL=https://api.testnet.hiro.so
NEXT_PUBLIC_STACKS_NETWORK=testnet
NEXT_PUBLIC_STACKS_API=https://api.testnet.hiro.so
X402_NETWORK=base-sepolia
NEXT_PUBLIC_X402_NETWORK=base-sepolia
DEMO_MODE=true
NEXT_PUBLIC_DEMO_MODE=true
POOL_SEED_USTX=5000000STACKS_NETWORK=mainnet
STACKS_API_URL=https://api.hiro.so
NEXT_PUBLIC_STACKS_NETWORK=mainnet
NEXT_PUBLIC_STACKS_API=https://api.hiro.so
X402_NETWORK=base
NEXT_PUBLIC_X402_NETWORK=base
DEMO_MODE=false
NEXT_PUBLIC_DEMO_MODE=false
POOL_SEED_USTX=0| Variable | Testnet value | Mainnet value | Used by |
|---|---|---|---|
STACKS_PRIVATE_KEY |
ST... wallet key (64 hex) | SP... wallet key | deploy scripts |
DEPLOYER_ADDRESS |
ST... |
SP... |
display only |
STACKS_NETWORK |
testnet |
mainnet |
deploy scripts |
STACKS_API_URL |
https://api.testnet.hiro.so |
https://api.hiro.so |
deploy scripts |
HIRO_API_KEY |
optional | recommended | deploy + frontend |
NEXT_PUBLIC_STACKS_NETWORK |
testnet |
mainnet |
Leather address resolution, contract calls |
NEXT_PUBLIC_STACKS_API |
https://api.testnet.hiro.so |
https://api.hiro.so |
frontend read-only calls |
X402_NETWORK |
base-sepolia |
base |
content server payment gate |
NEXT_PUBLIC_X402_NETWORK |
base-sepolia |
base |
frontend viem chain selection |
DEMO_MODE |
true |
false |
content server |
NEXT_PUBLIC_DEMO_MODE |
true |
false |
frontend hooks + Nav EVM chip |
X402_WALLET_ADDRESS |
any MetaMask 0x addr |
funded Base mainnet addr | content server receives USDC |
NEXT_PUBLIC_X402_WALLET |
same | same | frontend payment prompt display |
NEXT_PUBLIC_ADMIN_ADDRESS |
ST... testnet addr |
SP... mainnet addr |
admin tab visibility + panel access |
NEXT_PUBLIC_PROVIDER_REGISTRY_ADDRESS |
written by deploy-phase1.js |
written by deploy-phase-5.js |
frontend contract calls |
NEXT_PUBLIC_STAMP_REGISTRY_ADDRESS |
written by deploy-phase1.js |
written by deploy-phase-5.js |
frontend contract calls |
NEXT_PUBLIC_REWARD_POOL_ADDRESS |
written by deploy-phase-3.js |
written by deploy-phase-5.js |
frontend contract calls |
POOL_SEED_USTX |
5000000 (5 STX) |
0 |
phase 3 deploy seed |
ORACLE_PRIVATE_KEY |
from npm run oracle:keygen |
same key or new | oracle signing |
ORACLE_URL |
http://localhost:3002 |
hosted oracle URL | Next.js oracle proxy |
STAMP_JWT_SECRET |
any string | long random string | content server JWT |
SBTC_TOKEN_CONTRACT |
not used on testnet | SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token |
reward pool mainnet claim |
Follow these steps in order before running locally.
- Install Leather in Chrome
- Create a wallet. Write down your seed phrase.
- In Leather → Settings → Network → switch to Testnet
- Copy your
ST...testnet address - Request testnet STX at Hiro faucet
- Wait ~30 seconds — Leather balance should show STX
- Export your private key: Leather → Secret Key → copy
In .env:
STACKS_PRIVATE_KEY=<64-char hex key>
DEPLOYER_ADDRESS=ST<your address>
NEXT_PUBLIC_ADMIN_ADDRESS=ST<your address>Verify: curl "https://api.testnet.hiro.so/v2/accounts/ST<your-address>?proof=0" — balance should be non-zero.
npm run oracle:keygenExample output:
Private key (add to .env as ORACLE_PRIVATE_KEY):
a1b2c3...64 hex chars
Signing key hash (sha256 of pubkey -- register in provider-registry.clar):
0xdeadbeef...64 hex chars
In .env:
ORACLE_PRIVATE_KEY=a1b2c3...Save the signing key hash — you paste it into the Admin Panel when approving providers.
Verify: Start the oracle with npm run oracle:dev, then:
curl http://localhost:3002/health
# → { ok: true, keyHash: "0x...", publicKey: "0x..." }# Phase 1: provider-registry + stamp-registry
node deploy/deploy-phase1.js
# Takes ~3 minutes. Outputs:
# NEXT_PUBLIC_PROVIDER_REGISTRY_ADDRESS=ST...
# NEXT_PUBLIC_STAMP_REGISTRY_ADDRESS=ST...
# Written to frontend/.env.contracts
# Phase 3: reward-pool + seed with 5 STX
node deploy/deploy-phase-3.js
# Takes ~2 minutes. Outputs:
# NEXT_PUBLIC_REWARD_POOL_ADDRESS=ST...Copy the three addresses from frontend/.env.contracts into your .env:
NEXT_PUBLIC_PROVIDER_REGISTRY_ADDRESS=ST<deployer>.provider-registry
NEXT_PUBLIC_STAMP_REGISTRY_ADDRESS=ST<deployer>.stamp-registry
NEXT_PUBLIC_REWARD_POOL_ADDRESS=ST<deployer>.reward-poolNote: the frontend auto-loads frontend/.env.contracts and frontend/.env.contracts.phase3 via frontend/next.config.js, but you must restart npm run frontend:dev (or npm run dev) after deploying so the new values get picked up. Copying the values into .env is optional.
Verify: Open Hiro Explorer (testnet) and search ST<your-address>.stamp-registry — the contract should be visible.
Free at platform.hiro.so. Prevents rate limiting during rapid testing:
HIRO_API_KEY=hiro_...Skip this for basic testing — keep DEMO_MODE=true.
If you want to test real x402 payment flows:
- Switch MetaMask to Base Sepolia (chainId 84532)
- Get test USDC at faucet.circle.com → select Base Sepolia
- In
.env:X402_WALLET_ADDRESS=0x<your MetaMask addr> NEXT_PUBLIC_X402_WALLET=0x<same> DEMO_MODE=false NEXT_PUBLIC_DEMO_MODE=false
Three terminals:
# Terminal 1
npm run oracle:dev # → http://localhost:3002
# Terminal 2
npm run content-server:dev # → http://localhost:3001
# Terminal 3
npm run frontend:dev # → http://localhost:3000Or all in one with:
npm run dev- Open
http://localhost:3000 - Click Connect Wallet → Leather popup
- Approve the connection
- Your
ST...address appears in the nav bar
Check admin tab: The "Admin ⚙" tab is only visible if your connected address exactly matches NEXT_PUBLIC_ADMIN_ADDRESS. For any other wallet, it is completely invisible.
- Providers tab renders provider cards
- My Stamps tab shows empty gallery (stamps load after minting)
- Both work without wallet connection
- Click Earn Stamp
- Select a provider (e.g. The Green Lodge)
- Enter a booking reference ≥ 4 chars:
TEST-2026-001 - Click Validate & Mint Stamp
Expected step sequence:
| Step | What happens |
|---|---|
| Validating… | Frontend POSTs to /api/oracle-proxy → oracle verifies booking ref |
| Oracle signing proof… | Oracle returns 65-byte secp256k1 signature |
| Minting stamp on-chain… | Leather popup opens: earn-stamp contract call |
| Confirm in Leather | Tx broadcasts to Stacks testnet |
| Stamp Minted ✓ | Shows stamp ID and eco points awarded |
Click View on Explorer ↗ — Hiro Explorer shows the earn-stamp tx with Success status.
With
DEMO_MODE=true(server-side), the oracle call returns random bytes.stamp-registry.claraccepts them becausesig-verification-enableddefaults tofalseon testnet — real secp256k1 checking is only activated afterenable-sig-verificationis called during Phase 5 mainnet deploy.
- Earn 1–2 stamps
- Click Impact
- Dashboard shows: eco points, tier (Bronze < 20 pts), pool balance, epoch countdown
To reach Silver and test claiming:
- Earn stamps from providers worth 3–5 pts each until total ≥ 20
- Or temporarily lower the threshold by redeploying with a modified contract
- Click Guides
- Click any guide card
- Payment modal appears: "Payment required: $0.0010"
With DEMO_MODE=true:
- Click Pay now → content loads instantly, no wallet interaction needed
- Content server logs show
200 OK
With DEMO_MODE=false (Base Sepolia):
- MetaMask opens on Base Sepolia
- Sign the USDC
TransferWithAuthorizationEIP-712 message - Content unlocks after the CDP facilitator verifies the signature
Verify: Check content server terminal — should log GET /guides/<slug> 200.
- Click Apply
- Complete the 3-step form (name, category, eco score)
- Review screen → click Submit Application
- Listing fee prompt:
$0.10 USDC - With
DEMO_MODE=true: submits immediately - Content server terminal logs
POST /provider-listing-fee 200
Requirement: Connected wallet must match NEXT_PUBLIC_ADMIN_ADDRESS exactly.
A. Approve a provider:
- Connect the admin wallet
- Click Admin ⚙ in the nav
- Pending tab — submitted applications appear here
- Click Approve on any provider
- In the modal, paste your oracle signing key hash (from
npm run oracle:keygen) - Click Confirm Approval → Leather popup:
approve-providertx - Confirm in Leather
- Provider moves from Pending → Approved tab
- Verify on Hiro Explorer:
provider-registry.approve-providertx =Success
B. Revoke a provider:
- Go to Approved tab
- Click Revoke → confirmation guard appears
- Confirm → Leather popup:
revoke-providertx - Provider moves to Revoked tab
C. Seed the reward pool:
- Go to Pool Seed tab
- Select an amount (e.g. 2 STX)
- Click Seed Pool → Leather popup:
admin-seed-pooltx - After confirmation, go to Impact dashboard — pool balance increases
- Disconnect wallet (✕ button next to address chip)
- Connect a different Leather wallet (not
NEXT_PUBLIC_ADMIN_ADDRESS) - Admin ⚙ tab is NOT visible in nav
- Navigate to
/?adminmanually — redirected to home - No admin UI is rendered under any circumstances
- Earn 7+ stamps (mix of providers for variety)
- Check Impact → eco points ≥ 20 (Silver tier)
- Pool balance > 0 (seed from Admin Panel if needed)
- Click Claim Reward → Leather popup:
claim-rewardtx - Confirm → pool balance decreases, claim success shown
- Try claiming again immediately → "Cooldown active, X blocks remaining"
The Admin Panel is gated by a single env var:
NEXT_PUBLIC_ADMIN_ADDRESS=ST<your-testnet-address>| Scenario | Admin tab | Panel content |
|---|---|---|
NEXT_PUBLIC_ADMIN_ADDRESS is blank |
Never shown | Access denied for everyone |
| Connected wallet ≠ admin address | Not shown | Access denied screen |
| Connected wallet = admin address | Shown (amber tint) | Full panel |
| Admin disconnects while on panel | Tab disappears | Redirects to home |
The guard runs at three independent layers:
- Nav.tsx — filters admin from the visible nav items
- page.tsx —
safeSetSectionrejects admin route;useEffectejects on disconnect - AdminPanel.tsx — renders access-denied screen as final backstop
STACKS_NETWORK=mainnet
STACKS_API_URL=https://api.hiro.so
NEXT_PUBLIC_STACKS_NETWORK=mainnet
NEXT_PUBLIC_STACKS_API=https://api.hiro.so
X402_NETWORK=base
NEXT_PUBLIC_X402_NETWORK=base
DEMO_MODE=false
NEXT_PUBLIC_DEMO_MODE=false
POOL_SEED_USTX=0
NEXT_PUBLIC_ADMIN_ADDRESS=SP<your-mainnet-address>
DEPLOYER_ADDRESS=SP<your-mainnet-address>
X402_WALLET_ADDRESS=0x<funded-base-mainnet-wallet>
NEXT_PUBLIC_X402_WALLET=0x<same>~3–5 STX needed for contract deploy fees.
node deploy/deploy-phase-5.jsDeploys updated stamp-registry (secp256k1-recover) and reward-pool (real sBTC ft-transfer), then calls set-provider-registry and enable-sig-verification automatically.
For each approved provider, go to Admin Panel → approve with the correct signing key hash from npm run oracle:keygen (or curl http://localhost:3002/public-key/<id>).
Admin Panel → Pool Seed → deposit real sBTC via deposit-reward.
- Earn stamp → mainnet tx on Hiro Explorer
- Buy guide → MetaMask signs, real USDC moves on Base
- Claim reward → real sBTC moves from pool
| Function | Args | Access |
|---|---|---|
apply-provider |
name, category, eco-score, signing-key-hash |
Public |
approve-provider |
provider-id, signing-key-hash |
CONTRACT_OWNER |
revoke-provider |
provider-id |
CONTRACT_OWNER |
get-provider |
provider-id |
Read-only |
get-signing-key-hash |
provider-id |
Read-only |
| Function | Args | Notes |
|---|---|---|
earn-stamp |
provider-id, booking-hash, booking-proof, eco-points |
Oracle proof required on mainnet |
get-balance |
owner, token-id |
SIP-013 |
get-tier |
user |
0=Bronze, 1=Silver, 2=Gold |
enable-sig-verification |
— | CONTRACT_OWNER; mainnet post-deploy |
set-provider-registry |
new-registry |
CONTRACT_OWNER |
| Function | Testnet args | Mainnet args |
|---|---|---|
admin-seed-pool |
amount |
removed |
claim-reward |
tier |
sbtc-contract, tier |
deposit-reward |
amount, note |
sbtc-contract, amount, note |
get-reward-summary |
user, tier |
user, tier |
"Oracle unreachable" when minting stamps
Make sure npm run oracle:dev is running and ORACLE_URL=http://localhost:3002 is in .env. With DEMO_MODE=true (the server-side var, not NEXT_PUBLIC_), the oracle proxy returns random bytes without calling the oracle, so you can test without it running.
"Content server unreachable" when loading guides
Start npm run content-server:dev. Check CONTENT_SERVER_URL=http://localhost:3001 in .env.
Leather doesn't open when minting
NEXT_PUBLIC_STAMP_REGISTRY_ADDRESS is blank. Deploy Phase 1 (npm run deploy:phase1) and restart the Next.js dev server so it can read frontend/.env.contracts, or copy the value into .env / frontend/.env.local.
Admin tab not visible
NEXT_PUBLIC_ADMIN_ADDRESS is not set, or does not exactly match the connected wallet address. The comparison is case-sensitive. After changing this var, restart the Next.js dev server (npm run frontend:dev).
Wrong address in Leather after connect
NEXT_PUBLIC_STACKS_NETWORK must match the network Leather is set to. If Leather is on Testnet, set NEXT_PUBLIC_STACKS_NETWORK=testnet. The wallet connect code resolves stxAddress.testnet or stxAddress.mainnet based on this var.
"nonce too low" during deploy A previous transaction is still pending. Wait 1–2 minutes and retry.
x402 payment rejected in production mode
Confirm MetaMask is on the correct chain: Base Sepolia (84532) for X402_NETWORK=base-sepolia, Base mainnet (8453) for X402_NETWORK=base. Confirm test USDC balance is sufficient.
Claim reward button disabled
Pool is empty (seed from Admin → Pool Seed), or eco points < 20 (Silver tier required on mainnet; testnet contract has MIN_CLAIM_TIER=1 too), or cooldown is active (1008 blocks ≈ 1 week — check Impact dashboard for blocks remaining).