StagePass · x402
Micropayment-gated backstage API where crew and VIPs pay 0.01 USDC per secure access verification, replacing static tokens with real-time, revocable entry streams settled instantly on Fuji.
The primitive.
The onchain primitive runs at the right moment in the flow and surfaces a clear, verifiable result that directors can act on without web3 jargon.
Why this primitiveTransforms access control from a binary ownership state into a continuous revenue primitive; every door scan or credential check becomes a monetizable event that prevents token sharing and enables granular usage analytics.
Required keys.
Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
The build prompt.
Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →
Build "StagePass" in ONE Lovable message. Single-page x402-native paid app on Avalanche Fuji.
CONCEPT
Micropayment-gated backstage API where crew and VIPs pay 0.01 USDC per secure access verification, replacing static tokens with real-time, revocable entry streams settled instantly on Fuji.
Discipline: Theater & Live Performance (access control).
Onchain primitive: x402 micropayments on Avalanche Fuji (USDC via EIP-3009). Why: Transforms access control from a binary ownership state into a continuous revenue primitive; every door scan or credential check becomes a monetizable event that prevents token sharing and enables granular usage analytics.
5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No Lovable Cloud, no database, no auth flows beyond Privy drop-in.
- NO smart contract deploy. x402 settles USDC via EIP-3009 `transferWithAuthorization` — the USDC contract already exists at 0x5425890298aed601595a70AB815c96711a31Bc65 on Avalanche Fuji (chainId 43113).
- Privy is the auth + signing layer (Google login + embedded wallet on Avalanche Fuji).
- At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY, only if AI is part of the idea).
- Skip tests, skip CI, skip docs. Ship the demo.
STACK
- React + Vite + TanStack Start (the template Lovable ships).
- Privy embedded wallet wraps <App /> in src/main.tsx:
// IMPORTANT: pass the real viem `avalancheFuji` Chain object — a { id, name } stub
// silently breaks Privy's chain transport for EIP-712 signing.
import { avalancheFuji } from 'viem/chains';
<PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
config={{ loginMethods:['google'], embeddedWallets:{createOnLogin:'users-without-wallets'},
defaultChain: avalancheFuji, supportedChains: [avalancheFuji] }}>
- viem public client uses Alchemy Avalanche Fuji — put the URL in src/data/rpc.json as
{ "avalancheFuji": "https://avax-fuji.g.alchemy.com/v2/<KEY>" } and use
createPublicClient({ chain: avalancheFuji, transport: http(rpc.avalancheFuji) }).
The default public RPC is rate-limited — Alchemy is required for USDC balanceOf.
TEN NON-OBVIOUS x402 RULES (get these wrong and it silently fails)
1. CORS: proxy the facilitator. Public x402 facilitators (including x402.payai.network) do NOT send
Access-Control-Allow-Origin. A direct browser fetch throws "TypeError: Failed to fetch" BEFORE
you see the 402. Wrap the upstream call in a same-origin TanStack server route at
src/routes/api/public/x402-proxy.ts and forward PAYMENT-SIGNATURE (request) + PAYMENT-RESPONSE (response).
2. x402 v2 envelope shape. The base64-encoded PAYMENT-SIGNATURE payload is NOT
{ scheme, network, payload } at top level — that's v1 and PayAI rejects it as invalid_payload.
It MUST be:
{ "x402Version": 2,
"accepted": { /* the full PaymentRequirement you picked, echoed back verbatim */ },
"payload": { "signature": "0x…",
"authorization": { from, to, value, validAfter, validBefore, nonce } } }
3. Network id is CAIP-2: "eip155:43113" for Avalanche Fuji (NOT "avalanche-fuji"). Match on this
when picking a requirement from accepts[].
4. Amount field renamed. v2 uses `amount` (atomic units, string), NOT v1's `maxAmountRequired`.
USDC has 6 decimals — "10000" = 0.01 USDC.
5. Header names are literal-cased and non-standard: PAYMENT-SIGNATURE (request) and PAYMENT-RESPONSE (response).
6. EIP-3009 domain MUST be read on-chain, NOT from requirement.extra. PayAI recomputes the
EIP-712 digest against the token contract's actual name() and version(). Fuji USDC's on-chain
name() is "USD Coin" (facilitators often advertise extra.name:"USDC") and version() doesn't
always match extra.version. Trusting extra.* yields `invalid_exact_evm_token_name_mismatch`
or `invalid_exact_evm_signature`. Read both with viem and cache by chainId:asset:
const erc20Meta = [
{ type:"function", name:"name", stateMutability:"view", inputs:[], outputs:[{type:"string"}] },
{ type:"function", name:"version", stateMutability:"view", inputs:[], outputs:[{type:"string"}] },
] as const;
const pub = createPublicClient({ chain: avalancheFuji, transport: http(rpc.avalancheFuji) });
const [name, version] = await Promise.all([
pub.readContract({ address: requirement.asset, abi: erc20Meta, functionName: "name" }),
pub.readContract({ address: requirement.asset, abi: erc20Meta, functionName: "version" }),
]);
// domain: { name, version, chainId: 43113, verifyingContract: requirement.asset }
Fall back to requirement.extra.{name,version} only if the on-chain read throws.
7. nonce is bytes32 random, generated client-side (crypto.getRandomValues(new Uint8Array(32)) → 0x-hex).
Never reuse. validAfter = now-60s, validBefore = now + maxTimeoutSeconds (default 300).
8. Sign via Privy embedded wallet's provider, NOT React hooks: get the EIP-1193 provider
(const provider = await embedded.getEthereumProvider()) and call
provider.request({ method: "eth_signTypedData_v4", params: [address, JSON.stringify(typedData)] }).
9. Alchemy RPC in src/data/rpc.json (rule above). USDC balanceOf against the default public RPC will flake.
10. Fund flow uses Circle faucet (https://faucet.circle.com/, choose Avalanche Fuji). Show the user's
Privy address prominently, link to the faucet, then a "Refresh balance" button. ~10s arrival.
FILE LAYOUT
src/data/x402.json { endpoint, proxy, usdcAddress: "0x5425890298aed601595a70AB815c96711a31Bc65",
chainId: 43113, network: "eip155:43113",
faucetUrl: "https://faucet.circle.com/",
explorer: "https://testnet.snowtrace.io" }
src/data/rpc.json { "avalancheFuji": "https://avax-fuji.g.alchemy.com/v2/<KEY>" }
src/lib/x402.ts fetchChallenge / pickRequirement / signPayment / fetchPaid
src/routes/api/public/x402-proxy.ts same-origin GET proxy (below)
src/routes/index.tsx demo UI: sign-in → fund → 4-step flow log
PROXY ROUTE (drop-in — copy verbatim):
```ts
// src/routes/api/public/x402-proxy.ts
import { createFileRoute } from "@tanstack/react-router";
import x402Cfg from "@/data/x402.json";
export const Route = createFileRoute("/api/public/x402-proxy")({
server: {
handlers: {
GET: async ({ request }) => {
const sig = request.headers.get("PAYMENT-SIGNATURE");
const upstream = await fetch(x402Cfg.endpoint, {
method: "GET",
headers: sig ? { "PAYMENT-SIGNATURE": sig } : {},
});
const body = await upstream.arrayBuffer();
const out = new Headers();
const ct = upstream.headers.get("content-type");
if (ct) out.set("Content-Type", ct);
const pr = upstream.headers.get("PAYMENT-RESPONSE");
if (pr) out.set("PAYMENT-RESPONSE", pr);
return new Response(body, { status: upstream.status, headers: out });
},
},
},
});
```
The /api/public/* prefix bypasses Lovable's published-site auth — desired for a demo endpoint.
USER FLOW (log every step in the UI so the user sees what happened)
1. Land on page → "Sign in with Google" (Privy) → embedded wallet auto-provisioned on Avalanche Fuji.
2. Fund: show wallet address + "Get 0.01 USDC on Avalanche Fuji" → link to https://faucet.circle.com/
→ "Refresh balance" button reads USDC balanceOf via viem + Alchemy RPC.
3. Click the primary action button for this idea (access control). App runs:
(a) Challenge — GET /api/public/x402-proxy → expect 402 → parse { x402Version:2, accepts:[…], error }.
Pick accepts[] where network === "eip155:43113" && scheme === "exact".
(b) Sign — Build EIP-3009 typed data (see rules 6–8), call Privy signTypedData,
wrap into the v2 envelope (rule 2), base64.
(c) Retry — GET /api/public/x402-proxy with header PAYMENT-SIGNATURE: <base64>.
(d) Settle — On 200, read PAYMENT-RESPONSE header, base64-decode →
{ success, transaction, network, payer }. Link tx to `${explorer}/tx/${transaction}`.
4. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"
FACILITATOR
Default endpoint: https://x402.payai.network/api/avalanche-fuji/paid-content (returns 402 with an
accepts[] challenge, settles 0.01 USDC per successful call, returns the protected payload).
REQUIRED SECRETS (Lovable → Project Settings → Secrets):
- PRIVY_APP_ID Google sign-in + embedded wallet. Docs: https://docs.privy.io/llms-full.txt
- ALCHEMY_AVALANCHE_FUJI_RPC_URL Free at https://dashboard.alchemy.com/ (create app, Avalanche Fuji,
copy HTTPS URL). Also expose as VITE_ALCHEMY_AVALANCHE_FUJI_RPC_URL
so the frontend viem client can read balances.
COMMON FAILURE MODES (fix these before shipping)
- "TypeError: Failed to fetch" at step (a): called the facilitator directly from the browser. Use the proxy.
- "invalid_payload" at step (c): sent v1 envelope. Wrap under `accepted` (rule 2).
- "invalid_exact_evm_signature": trusted requirement.extra.version — on-chain version()
differs. Read name() + version() from the token contract (rule 6).
- "invalid_exact_evm_token_name_mismatch": trusted extra.name ("USDC") — Fuji USDC's on-chain
name() is "USD Coin". Same fix as above.
- "insufficient_funds": wallet has ETH but no USDC. Fund via Circle faucet, then Refresh balance.
- Balance stays at 0 after faucet: reading against default public RPC. Wire Alchemy via rpc.json.
- expires_at errors on retry: clock skew or reused nonce. Generate fresh nonce + timestamps per attempt.
CREDIT (must appear in UI footer):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Market sizing.
Indicative figures for hackathon pitches — refine with your own research before raising.