Rusher Paywall
Every call to this endpoint costs 0.01 USDC on Base Sepolia — the buyer signs a TransferWithAuthorization message, the facilitator verifies, and the response header carries the on-chain tx hash.
The kernel.
An agent runs the protocol server-side and photographers get a signed, auditable result they can act on without leaving the app.
Why this primitiveBase x402 on Base Sepolia fits fine art exhibition logistics in Photography because photographers can settle 0.01 USDC via a signed EIP-3009 authorization before the rusher endpoint returns anything.
Required key.
Add this in your Lovable project under Settings → Secrets before pasting the prompt below.
The build prompt.
Paste into a fresh Lovable project. Make sure the key above is set first. read the build strategy →
Build "Rusher Paywall · x402" as a ONE-SHOT Lovable build. The participant has only
5 credits — this single message must produce a working demo with no follow-ups.
Single-page TanStack Start app. Cut scope ruthlessly.
CONCEPT
Lovable AI runs a fulfillment agent that pays for expedited printing via AP2 so artists meet gallery deadlines.
Reframed as a paid, agent-callable HTTP endpoint on Base Sepolia x402 (0.01 USDC via Privy embedded EVM wallet + EIP-3009 + PayAI facilitator).
Discipline: Photography (fine art exhibition logistics).
LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
The participant has FIVE Lovable credits for the whole build. This prompt MUST
ship a working demo on the FIRST message with zero follow-ups. Engineer for that.
- ONE TanStack Start app, ONE route (`src/routes/index.tsx`). No extra pages, no auth, no nav.
- ONE TanStack server route under `src/routes/api/public/` that runs the protocol.
- ONE client surface (a button + flow log) that triggers the payment and renders the result.
- NO database, NO Lovable Cloud, NO extra integrations beyond what's listed below.
- NO tests, NO docs pages, NO settings screens.
- Demo-fallback contract: every code path must boot with NO secrets and return
realistic simulated envelopes flagged `simulated: true`. Real signing activates
only when the user signs in and the wallet is funded.
PROTOCOL SURFACE — x402 on Base Sepolia (self-contained; participant has no skills)
CONFIG (src/data/x402.json) — copy exactly:
```json
{
"endpoint": "https://x402.payai.network/api/base-sepolia/paid-content",
"proxy": "/api/public/x402-proxy",
"usdcAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"chainId": 84532,
"network": "eip155:84532",
"faucetUrl": "https://faucet.circle.com/",
"explorer": "https://sepolia.basescan.org",
"amount": "10000",
"amountLabel": "0.01 USDC"
}
```
RPC (src/data/rpc.json) — Alchemy Base Sepolia (falls back to public if no key):
```json
{ "baseSepolia": "https://base-sepolia.g.alchemy.com/v2/demo" }
```
The public RPC is rate-limited; wire an Alchemy key when you have one.
SAME-ORIGIN PROXY (src/routes/api/public/x402-proxy.ts) — CRITICAL: PayAI facilitator
sends no CORS headers, so a direct browser fetch throws "Failed to fetch" BEFORE the
402 lands. Every request MUST route through this same-origin proxy. Forward
`PAYMENT-SIGNATURE` (request) and `PAYMENT-RESPONSE` (response) verbatim:
```ts
import { createFileRoute } from "@tanstack/react-router";
import x402Cfg from "@/data/x402.json";
/** Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 */
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);
out.set("Access-Control-Expose-Headers", "PAYMENT-RESPONSE");
return new Response(body, { status: upstream.status, headers: out });
},
},
},
});
```
The `/api/public/*` prefix bypasses Lovable's published-site auth gate — that's
the design. Do NOT add auth middleware here.
FOUR-STEP FLOW (client lib src/lib/x402.ts + UI in src/routes/index.tsx):
1. CHALLENGE — `GET /api/public/x402-proxy` → expect HTTP 402 with JSON
`{ x402Version: 2, accepts: [PaymentRequirement, ...], error }`. Pick the
`accepts[]` entry where `network === "eip155:84532"` AND `scheme === "exact"`.
Amount is `req.amount` (atomic string) — for USDC 6-decimals, `"10000"` = 0.01 USDC.
NOTE: v2 uses `amount`, NOT v1's `maxAmountRequired`. Network id is CAIP-2
(`"eip155:84532"`), NOT `"base-sepolia"`.
2. SIGN — Build EIP-3009 `TransferWithAuthorization` typed data and sign it via
the Privy embedded EVM wallet's EIP-1193 provider (NOT React hooks):
```ts
const provider = await embedded.getEthereumProvider();
const sig = await provider.request({
method: "eth_signTypedData_v4",
params: [address, JSON.stringify(typedData)],
});
```
Typed data domain uses `chainId: 84532`, `verifyingContract: req.asset`,
and — CRITICAL — `name` and `version` come from `req.extra.name` /
`req.extra.version` (e.g. `"USDC"` / `"2"`), NEVER hardcoded to `"1"`.
Message fields:
- `from` = the Privy wallet address
- `to` = `req.payTo`
- `value` = `req.amount` (string, atomic units)
- `validAfter` = `Math.floor(Date.now()/1000) - 60`
- `validBefore` = `Math.floor(Date.now()/1000) + (req.maxTimeoutSeconds ?? 300)`
- `nonce` = fresh 32-byte hex (`crypto.getRandomValues(new Uint8Array(32))` → `"0x…"`)
NEVER reuse a nonce.
3. RETRY — Wrap the signed authorization into the x402 v2 envelope. CRITICAL:
v2 wraps under `accepted`, echoing the FULL PaymentRequirement you picked.
A top-level `{scheme, network, payload}` (v1 shape) is rejected as
`invalid_payload`:
```json
{
"x402Version": 2,
"accepted": { /* the full PaymentRequirement you picked, verbatim */ },
"payload": {
"signature": "0x…",
"authorization": { "from": "…", "to": "…", "value": "10000",
"validAfter": "…", "validBefore": "…", "nonce": "0x…" }
}
}
```
Base64-encode this JSON, send as `PAYMENT-SIGNATURE` header (exact case),
retry `GET /api/public/x402-proxy`.
4. SETTLE — On 200, read the `PAYMENT-RESPONSE` response header, base64-decode
to `{ success: true, transaction: "0x…", network: "eip155:84532", payer: "0x…" }`.
Link the tx to `${explorer}/tx/${transaction}`.
PRIVY MOUNT (SSR-safe, non-negotiable):
- NEVER import `@privy-io/react-auth` at module scope of a route or a file
reachable from `src/router.tsx` — it crashes workerd under SSR.
- Load it via `lazy(() => import("./privy-client-entry"))` inside a
`<ClientOnly>` + `<Suspense>` boundary in `src/components/privy-root.tsx`.
- Configure with `loginMethods: ['email', 'google']` and
`embeddedWallets: { ethereum: { createOnLogin: 'users-without-wallets' } }`.
- Read the wallet with `useWallets()`; get the EIP-1193 provider with
`await wallets[0].getEthereumProvider()`.
USER FLOW (the entire app):
1. Sign In (email/google) — Privy creates an embedded EVM wallet.
2. Show the wallet address + a "Fund with Circle faucet" link that opens
`https://faucet.circle.com/` with a "choose Base Sepolia + USDC" hint.
3. "Refresh USDC balance" button — reads balance via viem `createPublicClient`
against `rpcCfg.baseSepolia`.
4. "Unlock content" button — runs steps 1-4 above, streams each into a live
flow log with clear labels (Challenge · Sign · Retry · Settle).
5. On success, render the unlocked JSON payload + a Basescan link to the tx.
FAILURE MODES (surface the facilitator body verbatim in the flow log):
- `invalid_payload` → sent v1 envelope shape; wrap under `accepted` (step 3).
- `invalid_signature` → wrong `chainId` or hardcoded `extra.version`; use `84532`
and read `extra.version` from the picked requirement.
- `insufficient_funds` → wallet has ETH but no USDC; fund via Circle faucet.
- `TypeError: Failed to fetch` at step 1 → called facilitator directly, bypassing
the proxy; ALL requests must go through `/api/public/x402-proxy`.
SIMULATE MODE (default when user isn't signed in): the flow log walks through
the four steps with fake data flagged `simulated: true`, no real fetch, no
signing prompt. The button label switches from "Unlock (simulate)" to
"Unlock (live · Base Sepolia)" once the wallet is ready and funded.
STACK
- TanStack Start app, the index route only.
- Tailwind + shadcn. Editorial look: gold accent on a dark or warm-cream background,
generous type, one strong headline, one primary action.
- Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14".
- Libraries: template defaults + `@privy-io/react-auth` + `viem` + `zod`. Nothing else.
CREDIT (UI footer + JSDoc on the proxy handler): 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.