TagVerify · x402
Pay 0.01 USDC to cryptographically verify luxury accessory provenance against an on-chain registry, returning instant authenticity status and Fuji settlement proof as the trust primitive for resale platforms and AI shopping agents.
The primitive.
The onchain primitive runs at the right moment in the flow and surfaces a clear, verifiable result that fashion designers can act on without web3 jargon.
Why this primitiveShifts authentication from a static minting event to a dynamic, monetized verification query where payment gates access to truth, enabling high-frequency trust checks by bots and marketplaces without inventory friction.
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 "TagVerify" in ONE Lovable message. Single-page x402-native paid app on Avalanche Fuji.
CONCEPT
Pay 0.01 USDC to cryptographically verify luxury accessory provenance against an on-chain registry, returning instant authenticity status and Fuji settlement proof as the trust primitive for resale platforms and AI shopping agents.
Discipline: Fashion & Textile Design (accessory authentication).
Onchain primitive: x402 micropayments on Avalanche Fuji (USDC via EIP-3009). Why: Shifts authentication from a static minting event to a dynamic, monetized verification query where payment gates access to truth, enabling high-frequency trust checks by bots and marketplaces without inventory friction.
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 (accessory authentication). 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.