Signed Matchmaker
photographers hits the endpoint, gets a 402 with the payment requirement, and signs a 0.01 USDC EIP-3009 authorization from a Privy embedded wallet before PayAI settles on Base Sepolia.
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 brand partnerships in Photography because photographers can settle 0.01 USDC via a signed EIP-3009 authorization before the matchmaker 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 "Signed Matchmaker · 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 the agent that secures gear brand micro-sponsorships via UCP Checkout so creators get verified, paid brand deals.
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 (brand partnerships).
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.