📷 Photography · licensing exchange

SNAPSHOT · x402

A high-fidelity photo licensing protocol where human photographers and AI training agents pay 0.01 USDC per 'view-to-capture' or 'prompt-to-license'. Each request triggers an EIP-3009 transfer, instantly settling rights via a Base tx hash. No subscriptions; pay only for the pixels you pipe into your project or model.

Optimism + x402 paywall· x402 native
Section · Onchain

The primitive.

full primer →

The onchain primitive runs at the right moment in the flow and surfaces a clear, verifiable result that photographers can act on without web3 jargon.

Why this primitiveMoving from 'marketplaces' to 'metered access' solves the friction of bulk licensing. By turning the license into a per-call micropayment, creators get paid for every single impression/pull, and consumers avoid the overhead of large legal contracts.

Kernel
an x402 pay-per-use paywall on Optimism Sepolia — Privy embedded wallet signs EIP-3009 USDC authorizations, a same-origin proxy relays PAYMENT-SIGNATURE to a facilitator (self-hosted facilitator) that settles 0.01 USDC and returns an Optimism tx hash in PAYMENT-RESPONSE
Drives the UI as
a 4-step flow log (Challenge → Sign → Retry → Settle) that ends with a live Optimism Etherscan tx link proving the micropayment cleared
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on Optimism Sepolia via the Optimism faucet.
open ↗
OPTIMISM_SEPOLIA_RPC_URL
Alchemy Optimism Sepolia HTTPS endpoint (or https://sepolia.optimism.io).
open ↗
ETHERSCAN_API_KEY
Single Etherscan v2 key — covers Optimism Etherscan (chainId 11155420) with no extra key.
open ↗
PRIVY_APP_ID
Enables Google sign-in and sponsored Optimism Sepolia transactions.
open ↗
PINATA_JWT
Pins images / JSON / manifests to IPFS.
open ↗

Add these in your Lovable project under Settings → Secrets before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →

Build "SNAPSHOT" in ONE Lovable message. Single-page x402-native paid app on Optimism Sepolia.

CONCEPT
A high-fidelity photo licensing protocol where human photographers and AI training agents pay 0.01 USDC per 'view-to-capture' or 'prompt-to-license'. Each request triggers an EIP-3009 transfer, instantly settling rights via a Base tx hash. No subscriptions; pay only for the pixels you pipe into your project or model.
Discipline: Photography (licensing exchange).
Onchain primitive: x402 micropayments on Optimism Sepolia (USDC via EIP-3009). Why: Moving from 'marketplaces' to 'metered access' solves the friction of bulk licensing. By turning the license into a per-call micropayment, creators get paid for every single impression/pull, and consumers avoid the overhead of large legal contracts.

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 0x5fd84259d66Cd46123540766Be93DFE6D43130D7 on Optimism Sepolia (chainId 11155420).
- Privy is the auth + signing layer (Google login + embedded wallet on Optimism Sepolia).
- 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:
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google'], embeddedWallets:{createOnLogin:'users-without-wallets'},
                defaultChain:{ id: 11155420, name:'Optimism Sepolia' } }}>
- viem public client uses Alchemy Optimism Sepolia — put the URL in src/data/rpc.json as
  { "optimismSepolia": "https://opt-sepolia.g.alchemy.com/v2/<KEY>" } and use
  createPublicClient({ chain: optimismSepolia, transport: http(rpc.optimismSepolia) }).
  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 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 the facilitator will reject 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:11155420" for Optimism Sepolia (NOT "optimism-sepolia"). 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 fields come from requirement.extra. Use extra.name ("USDC") and extra.version ("2")
   in the EIP-712 domain — NOT hardcoded. chainId is 11155420; verifyingContract is requirement.asset.
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 Optimism Sepolia). 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: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
                                chainId: 11155420, network: "eip155:11155420",
                                faucetUrl: "https://faucet.circle.com/",
                                explorer: "https://sepolia-optimism.etherscan.io" }
  src/data/rpc.json           { "optimismSepolia": "https://opt-sepolia.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 Optimism Sepolia.
2. Fund: show wallet address + "Get 0.01 USDC on Optimism Sepolia" → 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 (licensing exchange). App runs:
   (a) Challenge — GET /api/public/x402-proxy → expect 402 → parse { x402Version:2, accepts:[…], error }.
       Pick accepts[] where network === "eip155:11155420" && 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: /api/public/x402-endpoint (self-hosted, same-origin — code below) (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_OPTIMISM_SEPOLIA_RPC_URL  Free at https://dashboard.alchemy.com/ (create app, Optimism Sepolia,
                                copy HTTPS URL). Also expose as VITE_ALCHEMY_OPTIMISM_SEPOLIA_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_signature": wrong domain — chainId mismatch, or extra.version hardcoded to "1".
  Use chainId 11155420 and extra.version from the requirement.
- "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
Appendix · Market

Market sizing.

TAM
$15B
Global digital rights management and AI training data procurement.
SAM
$1.2B
The professional stock photography and real-time news imagery market.
SOM
$45M
On-chain image delivery for decentralized social (Lens/Farcaster) and AI model ingestion.

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.