🎬 Filmmaking & Animation · distributed rendering

FRAMESET · x402

A hyper-granular rendering marketplace where animation frames are treated as unit-commodities. Creators pay 0.01 USDC per frame rendered by a distributed node network via EIP-3009. No subscriptions or bulk credits—just pure, pay-per-frame settlement that allows hobbyists to clear small batches and studios to burst-render at scale without overhead. Render workers receive instant, per-frame micro-settlements, turning idle GPU cycles into real-time USDC streams.

BOB Sepolia + 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 filmmakers can act on without web3 jargon.

Why this primitiveTraditional render farms have high entry thresholds ($50 minimums) and opaque pricing. By normalizing all rendering tasks to a 0.01 USDC base unit per frame (or complexity unit), we create a high-velocity 'render-as-you-go' economy ideal for the growing indie animation and AI-video sectors.

Kernel
an x402 v2 paywall that meters access with 0.01 USDC per call via EIP-3009 authorization, signed by an external MetaMask EOA (connected through Privy) and settled by a self-hosted facilitator on BOB Sepolia
Drives the UI as
a 'pay 0.01 USDC to unlock' button that returns the BOB Sepolia tx hash and links straight to BOB Sepolia Explorer
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on BOB Mainnet via the Google Cloud faucet.
open ↗
ROOTSTOCK_TESTNET_RPC_URL
Alchemy BOB Mainnet HTTPS endpoint. Create a free app → copy the HTTPS URL.
open ↗
ROOTSTOCK_EXPLORER_API_KEY
Required for npx hardhat verify after deploy.
open ↗
PRIVY_APP_ID
Enables Google sign-in and faucet-funded 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 "FRAMESET" in ONE Lovable message. Single-page x402-native paid app on BOB Sepolia.

CONCEPT
A hyper-granular rendering marketplace where animation frames are treated as unit-commodities. Creators pay 0.01 USDC per frame rendered by a distributed node network via EIP-3009. No subscriptions or bulk credits—just pure, pay-per-frame settlement that allows hobbyists to clear small batches and studios to burst-render at scale without overhead. Render workers receive instant, per-frame micro-settlements, turning idle GPU cycles into real-time USDC streams.
Discipline: Filmmaking & Animation (distributed rendering).
Onchain primitive: x402 micropayments on BOB Sepolia (native Circle USDC via EIP-3009). Why: Traditional render farms have high entry thresholds ($50 minimums) and opaque pricing. By normalizing all rendering tasks to a 0.01 USDC base unit per frame (or complexity unit), we create a high-velocity 'render-as-you-go' economy ideal for the growing indie animation and AI-video sectors.

NETWORK — BOB Sepolia (testnet, required verbatim)
- chainId 808813 (hex 0xc57cd). BOB is an OP-Stack L2 secured by Bitcoin.
- Gas token is ETH. Never BTC, and never the BOB ERC-20 token — holding BOB on
  Ethereum L1 does not pay for gas on the BOB network.
- RPC:      https://bob-sepolia.rpc.gobob.xyz
- Explorer: https://bob-sepolia.explorer.gobob.xyz  (Blockscout, not Etherscan)
- Funding: claim Sepolia ETH at https://sepolia-faucet.pk910.de/ then bridge to
  BOB Sepolia at https://bob-sepolia.gobob.xyz/
- Reference: https://docs.gobob.xyz/docs/user-hub/networks
- CAIP-2 network id for the x402 envelope: eip155:808813. Amounts are atomic
  with 6 decimals, so "10000" = 0.01 USDC.
- Blockscout never auto-verifies: run `npx hardhat verify --network bobSepolia <address>`
  right after deploy, keeping a dummy `apiKey: { bobSepolia: "empty" }` entry.


5-CREDIT BUDGET (HARD LIMIT)
- ONE single-page app. No Lovable Cloud, no database, no auth flows beyond the Privy drop-in.
- DEPLOY ONE MockUSDC (EIP-3009 compliant, name "USDC", version "2", 6 decimals) to BOB Sepolia via Hardhat, then
  seed the demo wallet with balance in the constructor. BOB Sepolia has no canonical Circle USDC — you own the token.
  Facilitator settles `transferWithAuthorization` against YOUR deployed MockUSDC address (chainId 808813).
- Self-host the facilitator as ONE TanStack server route inside the same app (shape below).
- 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 wraps <App />. The signing wallet MUST be an external EOA (MetaMask), NOT a Privy embedded wallet — see rule 1.
    import { bobSepolia } from '@/lib/chains'; // custom viem Chain: id 808813, rpc https://bob-sepolia.rpc.gobob.xyz, explorer https://bob-sepolia.explorer.gobob.xyz
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google','email','wallet'],
                embeddedWallets:{ ethereum:{ createOnLogin:'users-without-wallets' } },
                defaultChain: bobSepolia,
                supportedChains: [bobSepolia] }}>
- viem public client uses the BOB Sepolia RPC. Put it in src/data/rpc.json as
    { "bobSepolia": "https://bob-sepolia.rpc.gobob.xyz" }
  and use createPublicClient({ chain: bobSepolia, transport: http(rpc.bobSepolia) }).
  The default public RPC rate-limits balance reads and over-estimates gas.

THIRTEEN NON-OBVIOUS RULES (get any of these wrong and the demo silently fails)

1. DO NOT sign x402 with the Privy embedded wallet. Privy embeds are EIP-7702 delegated EOAs — the USDC v2.2
   SignatureChecker sees on-chain code at the signer address, routes to ERC-1271 isValidSignature, and reverts
   with "FiatTokenV2: invalid signature" (payer_is_smart_account). x402 signing MUST come from an external EOA
   (MetaMask). Connect it via Privy's "wallet" login method after Google/email sign-in.

2. Self-host the facilitator as ONE route. GET /api/public/x402-paid-content:
     - no PAYMENT-SIGNATURE header  → return 402 with { x402Version:2, accepts:[PaymentRequirement] }.
     - with header                   → decode envelope, verify EIP-3009, simulate, writeContract, wait,
                                       return 200 + PAYMENT-RESPONSE header (base64 JSON with
                                       { success, transaction, network, payer }).
   The /api/public/* prefix bypasses Lovable's published-site auth — that's intentional for the demo.

3. x402 v2 envelope shape (NOT v1's { scheme, network, payload } at top level — facilitator rejects that as
   invalid_payload). It MUST be:
     { "x402Version": 2,
       "accepted":   { /* echo the full PaymentRequirement you picked, verbatim */ },
       "payload":    { "signature": "0x…",
                       "authorization": { from, to, value, validAfter, validBefore, nonce } } }

4. Network id is CAIP-2: "eip155:808813" (NOT "bob-sepolia" or "bobSepolia"). Match on this when picking
   a requirement from accepts[]. Scheme is "exact".

5. Amount is atomic units, string. USDC has 6 decimals — "10000" = 0.01 USDC. Field name is `amount` (v2),
   NOT v1's maxAmountRequired.

6. Header names are literal-cased and non-standard: PAYMENT-SIGNATURE (request) and PAYMENT-RESPONSE (response).
   Read case-insensitively (fetch Headers is), but SEND exactly that casing.

7. Read the EIP-712 domain from the USDC contract via EIP-5267 — never hardcode name/version. Call
   eip712Domain() on the token (falls back to name() + version()), cache by (chainId, asset). Circle rotates
   versions across chains. Circle USDC on peer chains currently returns ("USDC", "2") but treat that as data, not truth.
   Put the on-chain values into the requirement's `extra: { name, version }` and thread them into the EIP-712
   domain used for signing AND for server-side recovery.

8. MetaMask eth_signTypedData_v4 REQUIRES "EIP712Domain" in the payload types. If you omit it, MetaMask hashes
   a different digest than viem's recoverTypedDataAddress — you get signer_mismatch on every attempt. Include:
     types: {
       EIP712Domain: [
         { name: "name",              type: "string"  },
         { name: "version",           type: "string"  },
         { name: "chainId",           type: "uint256" },
         { name: "verifyingContract", type: "address" },
       ],
       TransferWithAuthorization: [
         { name: "from",         type: "address" },
         { name: "to",           type: "address" },
         { name: "value",        type: "uint256" },
         { name: "validAfter",   type: "uint256" },
         { name: "validBefore",  type: "uint256" },
         { name: "nonce",        type: "bytes32" },
       ],
     }
   Only pass EIP712Domain in the RPC payload sent to the wallet. Do NOT include it in the types object
   you pass to viem's recoverTypedDataAddress — viem adds it itself.

9. Recover the signer BROWSER-SIDE before submitting. Right after eth_signTypedData_v4 returns, call viem's
   recoverTypedDataAddress on the same signature and check it matches
   provider.request({ method: "eth_accounts" })[0]. If not, show "select one account in MetaMask, reconnect,
   and retry". Thread authorization.from = recovered into the envelope so the server and MetaMask agree.

10. Force chain switch to BOB Sepolia BEFORE reading eth_accounts. Call
    provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0xc576d" }] }).catch(()=>{}).
    A wallet on the wrong chain will still sign, but the domain digest differs — recovery fails.

11. nonce is 32 random bytes generated client-side (crypto.getRandomValues(new Uint8Array(32)) → 0x-hex). Never
    reuse. validAfter = now - 60s, validBefore = now + (requirement.maxTimeoutSeconds ?? 300).

12. Server-side, before writeContract, ALWAYS do:
    (a) code = pub.getCode({ address: auth.from }); reject "unsupported_payer" if code !== "0x"
        (protects against a stray Privy embedded / smart account signature slipping through).
    (b) recovered = recoverTypedDataAddress(...); reject "signer_mismatch" if != auth.from.
    (c) authorizationState(from, nonce) === false and balanceOf(from) >= amount.
    (d) simulateContract(transferWithAuthorization) — if it throws, format the revert reason (regex out
        "reverted with the following reason: '...'") and return 402 with settle_preflight_failed.

13. Broadcast with PINNED gas: writeContract({ ..., gas: 250_000n }). Public BOB Sepolia RPC over-estimates
    intrinsic gas — you'll see "intrinsic gas too high" without a pin. After broadcast,
    receipt = waitForTransactionReceipt; check receipt.status === "success". Inclusion ≠ success — a revert
    still costs the relayer gas but should be surfaced as tx_reverted with the tx hash. On revert, re-run
    simulateContract and format the reason for the flow log.

FILE LAYOUT
  src/data/x402.json          { endpoint: "/api/public/x402-paid-content",
                                proxy:    "/api/public/x402-paid-content",
                                usdcAddress: "<address of your deployed MockUSDC on BOB Sepolia>",
                                payTo:    "<treasury or relayer EOA>",
                                chainId:  808813,
                                network:  "eip155:808813",
                                networkName: "BOB Sepolia",
                                amount:   "10000",
                                faucetUrl: "https://bob-sepolia.gobob.xyz/",
                                ethFaucetUrl: "https://bob-sepolia.gobob.xyz/",
                                explorer: "https://bob-sepolia.explorer.gobob.xyz" }
  src/data/rpc.json           { "bobSepolia": "https://bob-sepolia.rpc.gobob.xyz" }
  src/lib/x402.ts             fetchChallenge / pickRequirement / signPayment / fetchPaid
  src/routes/api/public/x402-paid-content.ts   self-hosted facilitator (challenge + verify + settle)
  src/routes/index.tsx        demo UI: sign-in → connect MetaMask → fund → 4-step flow log

FACILITATOR ROUTE (drop-in — the shape you must ship)
```ts
// src/routes/api/public/x402-paid-content.ts
import { createFileRoute } from "@tanstack/react-router";
import x402Cfg from "@/data/x402.json";
import {
  createPublicClient, createWalletClient, http, parseAbi,
  parseSignature, recoverTypedDataAddress, type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains"; import { bobSepolia } from "@/lib/chains";

const ABI = parseAbi([
  "function transferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce,uint8 v,bytes32 r,bytes32 s)",
  "function authorizationState(address,bytes32) view returns (bool)",
  "function balanceOf(address) view returns (uint256)",
  "function eip712Domain() view returns (bytes1,string,string,uint256,address,bytes32,uint256[])",
  "function name() view returns (string)",
  "function version() view returns (string)",
]);
const CORS = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, PAYMENT-SIGNATURE",
  "Access-Control-Expose-Headers": "PAYMENT-RESPONSE, PAYMENT-REQUIRED",
};
// Follow rules 2–13 above inside the GET handler. Return 402 with accepts[] when no header;
// otherwise decode → checks (12a–d) → writeContract({...,gas:250_000n}) → waitForTransactionReceipt
// → 200 with PAYMENT-RESPONSE header on success, 402 with settle_reverted otherwise.
export const Route = createFileRoute("/api/public/x402-paid-content")({
  server: { handlers: { OPTIONS: async () => new Response(null,{status:204,headers:CORS}),
                        GET: async ({ request }) => handle(request) } },
});
```

SIGNING BRIDGE (drop-in — MetaMask via Privy)
```ts
const provider = await selectedWallet.getEthereumProvider();
await provider.request({ method:"wallet_switchEthereumChain",
                         params:[{ chainId:"0xc576d" }] }).catch(()=>{});
const [active] = await provider.request({ method:"eth_accounts" });
const from = getAddress(active);
const finalized  = { ...typedData, message: { ...typedData.message, from } };
const rpcPayload = { ...finalized, types: { EIP712Domain: [...], ...finalized.types } }; // rule 8
const signature  = await provider.request({ method:"eth_signTypedData_v4",
                                            params:[from, JSON.stringify(rpcPayload)] });
const recovered  = await recoverTypedDataAddress({ ...finalized, signature });
if (recovered.toLowerCase() !== from.toLowerCase())
  throw new Error("Select one account in MetaMask, reconnect, and retry.");
return { signature, from, message: finalized.message };
```

USER FLOW (log every step in the UI)
1. Land on page → "Sign in" (Privy Google/email) → "Connect wallet" → MetaMask.
2. Fund: show MetaMask address + two faucet links —
   - USDC: https://faucet.circle.com/ (choose BOB Sepolia)
   - ETH:  https://bob-sepolia.gobob.xyz/
   → "Refresh balance" reads USDC balanceOf via viem + Alchemy RPC.
3. Primary action for this idea (distributed rendering). App runs:
   (a) Challenge — GET /api/public/x402-paid-content → expect 402 → parse
       { x402Version:2, accepts:[…] }. Pick where network==="eip155:808813" && scheme==="exact".
   (b) Sign — Build EIP-3009 typed data (rules 7, 8), sign via MetaMask through the bridge above,
       wrap into the v2 envelope (rule 3), base64.
   (c) Retry — GET /api/public/x402-paid-content 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"

REQUIRED SECRETS (Lovable → Project Settings → Secrets)
- PRIVY_APP_ID (+ VITE_PRIVY_APP_ID)          Google/email login + external wallet connect. Docs: https://docs.privy.io/llms-full.txt
- RELAYER_PRIVATE_KEY                          Funded EOA that pays ETH gas for transferWithAuthorization.
                                               Fund at https://bob-sepolia.gobob.xyz/
                                               Same address goes in x402.json's payTo so the treasury receives the USDC.
- BOB_SEPOLIA_RPC_URL (+ VITE_BOB_SEPOLIA_RPC_URL)     Free HTTPS URL from https://dashboard.alchemy.com/ (create app,
                                               BOB Sepolia). Public RPC rate-limits and over-estimates gas.

FAILURE-MODE TABLE (fix these before shipping)
- "TypeError: Failed to fetch"                     → You're calling a third-party facilitator, not the same-origin route. Use /api/public/x402-paid-content.
- "intrinsic gas too high"                          → No gas pin on writeContract. Set gas: 250_000n.
- "invalid_signature: signer_mismatch"              → Missing EIP712Domain in the RPC payload (rule 8), or MetaMask signed with a different account than eth_accounts[0] (rule 9).
- "payer_is_smart_account" / isValidSignature revert → You signed with the Privy embedded wallet. Connect MetaMask instead (rule 1).
- "settle_reverted: FiatTokenV2: invalid signature"  → Domain name/version hardcoded, doesn't match on-chain. Read via EIP-5267 (rule 7).
- "insufficient_funds"                              → Wallet has ETH but no USDC. Hit the Circle faucet, then Refresh balance.
- "nonce_already_used" / expires_at errors           → Reused envelope or clock skew. Regenerate nonce + timestamps per attempt (rule 11).
- Balance stuck at 0 after Circle faucet             → Reading via default public RPC. Wire Alchemy in rpc.json.
- "invalid_payload"                                  → Sent v1 envelope shape. Wrap under `accepted` (rule 3).

DESIGN DIRECTION
Commit to one distinctive look drawn from filmmaking & animation — specifically distributed rendering.
No purple-on-white gradient hero, no Inter/Poppins default stack, no generic three-card
feature row. Pick a real typeface pairing, one accent colour plus a near-monochrome base,
and a layout with an obvious focal point. Define colours as CSS variables/tokens; never
hardcode text-white or bg-black. Dark by default. The proof elements (payer address, USDC
balance, settlement tx hash) should read as deliberate mono-type detail, not debug output.

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
$35B
The global 3D visualization and animation software ecosystem transitioning to cloud-native workflows.
SAM
$4.8B
The cloud rendering services market for independent studios and freelance 3D artists.
SOM
$85M
Real-time frame settlement for decentralized animation collectives and AI-driven video synthesis workers on Base.高速渲染.

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

See also

Adjacent entries.