🎬 Video & Streaming · script and dialogue

WriterGuard · x402

A high-frequency dialogue versioning and script-doctoring engine. Pay 0.01 USDC to commit a dialogue polish, branch a scene, or resolve a revision conflict via the x402 settle-and-lock model. No monthly subscriptions for film crews—just pay for the granular creative decisions that make the final cut. Every transaction provides a Base hash as an immutable proof-of-authorship for WGA/Royalty tracking.

Ethereum 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 video creators can act on without web3 jargon.

Why this primitiveScript writing is a series of thousands of micro-decisions. By making 'the edit' the atomic unit of payment, the protocol captures value from professional writer's rooms and AI script-doctoring agents who need to verify contribution history without the friction of SaaS overhead.

Kernel
Self-hosted x402 v2 facilitator settles USDC via EIP-3009 transferWithAuthorization on Sepolia; MetaMask signs, relayer pays gas.
Drives the UI as
Sign in with Privy, connect MetaMask, click to unlock — the facilitator returns a PAYMENT-RESPONSE tx hash.
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Sepolia deployer + fisher wallet (single-EOA model). Fund via the Google Cloud faucet.
open ↗
SEPOLIA_RPC_URL
Alchemy Sepolia HTTPS endpoint. Public RPCs throttle under hackathon load.
open ↗
ETHERSCAN_API_KEY
Required for forge verify-contract after deploy.
open ↗
PRIVY_APP_ID
Enables Google sign-in and sponsored transactions. Toggle Gas sponsorship → Sepolia in the dashboard.
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 "WriterGuard" in ONE Lovable message. Single-page x402-native paid app on Ethereum Sepolia.

CONCEPT
A high-frequency dialogue versioning and script-doctoring engine. Pay 0.01 USDC to commit a dialogue polish, branch a scene, or resolve a revision conflict via the x402 settle-and-lock model. No monthly subscriptions for film crews—just pay for the granular creative decisions that make the final cut. Every transaction provides a Base hash as an immutable proof-of-authorship for WGA/Royalty tracking.
Discipline: Videography & Film (script and dialogue).
Onchain primitive: x402 micropayments on Ethereum Sepolia (native Circle USDC via EIP-3009). Why: Script writing is a series of thousands of micro-decisions. By making 'the edit' the atomic unit of payment, the protocol captures value from professional writer's rooms and AI script-doctoring agents who need to verify contribution history without the friction of SaaS overhead.

5-CREDIT BUDGET (HARD LIMIT)
- ONE single-page app. No Lovable Cloud, no database, no auth flows beyond the Privy drop-in.
- NO smart contract deploy. x402 settles USDC via EIP-3009 `transferWithAuthorization` on the existing
  Circle USDC contract 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 (Ethereum Sepolia, chainId 11155111).
- 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.

SEPOLIA UX GUARDRAILS (session learnings — apply on top of the x402 rules above)

- Privy sponsored host-chain sends: any user tx (including EIP-3009 authorization broadcast on
  the fallback path) MUST be sent as:
      sendTransaction({ to, data, chainId: 11155111 }, { sponsor: true, address: embedded.address });
  Both `sponsor: true` and `address: embedded.address` are required or Privy silently routes
  through a non-embedded wallet and prompts for gas. NEVER pass uiOptions:{ showWalletUIs: false }
  — it aborts with "signal is aborted without reason".

- Decimal USDC UX (non-negotiable): inputs are <input inputMode="decimal" defaultValue="0.01">.
  Parse with parseUnits(str.trim(), 6) → bigint atomic. Display every balance / amount as
  `${formatUnits(wei, 6)} USDC`. NEVER render raw atomic values like "10000" or "USDC (micro)"
  labels — users have called this out as confusing.

- Empty-state UX when wallet USDC balance is 0: render an amber notice containing
  (a) the exact wallet address to fund, (b) a "Copy address" button, (c) a link to the Circle
  Sepolia USDC faucet https://faucet.circle.com/, (d) a "Refresh balance" button. Do NOT let
  the primary "Pay $0.01" action be clickable while balance is 0 — it will revert / 402-loop.

- Facilitator settlement UI: when the facilitator returns a settled tx hash, render a clickable
  Sepolia Etherscan link built server-side (`https://sepolia.etherscan.io/tx/${txHash}`), not
  reconstructed client-side. Surface raw facilitator errors (invalid_payload, insufficient_funds,
  invalid_signature) verbatim in the step log — do NOT swallow them.

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 { sepolia } from 'viem/chains';
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google','email','wallet'],
                embeddedWallets:{ ethereum:{ createOnLogin:'users-without-wallets' } },
                defaultChain: sepolia,
                supportedChains: [sepolia] }}>
- viem public client uses Alchemy Sepolia. Put it in src/data/rpc.json as
    { "sepolia": "https://eth-sepolia.g.alchemy.com/v2/<KEY>" }
  and use createPublicClient({ chain: sepolia, transport: http(rpc.sepolia) }).
  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:11155111" (NOT "ethereum-sepolia" or "sepolia"). 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. Sepolia USDC 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 Ethereum Sepolia BEFORE reading eth_accounts. Call
    provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0xaa36a7" }] }).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 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: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
                                payTo:    "<treasury or relayer EOA>",
                                chainId:  11155111,
                                network:  "eip155:11155111",
                                networkName: "Ethereum Sepolia",
                                amount:   "10000",
                                faucetUrl: "https://faucet.circle.com/",
                                ethFaucetUrl: "https://cloud.google.com/application/web3/faucet/ethereum/sepolia",
                                explorer: "https://sepolia.etherscan.io" }
  src/data/rpc.json           { "sepolia": "https://eth-sepolia.g.alchemy.com/v2/<KEY>" }
  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";

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:"0xaa36a7" }] }).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 Ethereum Sepolia)
   - ETH:  https://cloud.google.com/application/web3/faucet/ethereum/sepolia
   → "Refresh balance" reads USDC balanceOf via viem + Alchemy RPC.
3. Primary action for this idea (script and dialogue). App runs:
   (a) Challenge — GET /api/public/x402-paid-content → expect 402 → parse
       { x402Version:2, accepts:[…] }. Pick where network==="eip155:11155111" && 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://cloud.google.com/application/web3/faucet/ethereum/sepolia
                                               Same address goes in x402.json's payTo so the treasury receives the USDC.
- SEPOLIA_RPC_URL (+ VITE_SEPOLIA_RPC_URL)     Free HTTPS URL from https://dashboard.alchemy.com/ (create app,
                                               Ethereum 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).

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
$1.8B
The entertainment IP creation market, inclusive of AI-assisted dialogue generation and distribution.
SAM
$250M
The global screenplay software and specialized creative collaboration niche.
SOM
$12M
Independent film productions and freelance script-doctors using micropayment-integrated version control.

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

See also

Adjacent entries.

video copyright
FrameGuard · x402
$0.01 — Per-frame attestation. A micropayment-metered verification engine for filmmakers. Protect your raw files by anchoring unique visual fingerprints to Base at the moment of capture. Producers pay per timestamp to generate a legally verifiable chain of custody, effectively ending deepfake disputes and metadata stripping. AI scraping agents must pay the x402 toll to verify rights before inclusion in training sets.
edit history
CUTSCENE · x402
A provable version-control layer for post-production. $0.01 per version commit creates an immutable, timestamped 'state-save' on Base. Pay to pull the history of any clip, ensuring editors get paid per revision and directors have a verifiable audit trail of every frame change. No more 'Final_Final_v2.mp4' confusion.
licensed assets
RawCut · x402
A granular video asset mart where creators pay-per-frame to license raw B-roll. No subscriptions, no bulky contracts—0.01 USDC triggers an instant EIP-3009 transfer that fetches a high-res, watermarked-removed download link via the facilitator. Ideal for short-form editors who need a single 3-second transition without buying a $50/mo library access.
color grading
CHROMA · x402
A professional-grade LUT extraction and application engine where users pay 0.01 USDC to instantly apply premium cinema-grade color science to their footage or export a mobile-ready filter. Each grading operation is an onchain event, ensuring creators get paid per frame processed or per preset unlocked.