🎬 Filmmaking & Animation · color grading proof

SceneMood Chain

Record color grading versions on blockchain to secure post-production creative decisions.

BOB Sepolia smart contract· onchain logic
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 primitiveBOB Sepolia contracts timestamp and preserve immutable color study records.

Kernel
a Solidity contract deployed to BOB Sepolia via MetaMask private key, then verified on BOB Sepolia Explorer
Drives the UI as
a 'verified onchain' badge with the live contract address and a BOB Sepolia Explorer link
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 "SceneMood Chain" in ONE Lovable message. Single-page demo. This prompt is self-contained: everything you need is below — do not ask follow-up questions.

CONCEPT
Record color grading versions on blockchain to secure post-production creative decisions.
Discipline: Filmmaking & Animation (color grading proof).
Onchain primitive: BOB Sepolia smart contract. Why this primitive: BOB Sepolia contracts timestamp and preserve immutable color study records.

5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no auth flows beyond Privy drop-in.
- ONE Solidity contract, <=80 lines, deployed to BOB Sepolia, verified on BOB Sepolia Explorer (Blockscout).
- Privy is always the auth + sponsored-tx layer (Google login, embedded wallet).
- Pinata/IPFS only if the idea genuinely needs to store a file or metadata.
- At most ONE AI call per user action (use Lovable AI Gateway with LOVABLE_API_KEY if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

NETWORK — BOB Sepolia (testnet, everything below is 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
  tokens 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 the deployer: claim Sepolia ETH at https://sepolia-faucet.pk910.de/
  then bridge it to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- Reference: https://docs.gobob.xyz/docs/user-hub/networks
- Create src/lib/chains.ts exactly like this:
```ts
import type { Chain } from "viem";

export const bobSepolia: Chain = {
  id: 808813,
  name: "BOB Sepolia",
  nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: {
    default: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
    public: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
  },
  blockExplorers: {
    default: { name: "BOB Sepolia Explorer", url: "https://bob-sepolia.explorer.gobob.xyz" },
  },
  testnet: true,
};

export const EXPLORER = "https://bob-sepolia.explorer.gobob.xyz";
```

FILE LAYOUT (create every one of these — nothing else)
```text
contracts/SceneMoodChain.sol          the contract source below
hardhat.config.cjs              Blockscout verify config below
scripts/deploy.cjs              deploy script below
src/lib/chains.ts               bobSepolia viem Chain (above)
src/lib/contract.ts             ABI + address helper, viem public client
src/data/contract.json          { "address": "0x…", "deployTx": "0x…" } written after deploy
src/components/privy-root.tsx   ClientOnly + Suspense + lazy wrapper
src/components/privy-client-entry.tsx  the ONLY file importing @privy-io/react-auth
src/pages/Index.tsx             the entire demo UI (single page)
```

STACK
- React + Vite single page (the index route).
- SSR-safe Privy mount is mandatory. Never import @privy-io/react-auth at
  module scope of a route file — it crashes SSR. Use
  lazy(() => import('./privy-client-entry')) inside <ClientOnly> + <Suspense>,
  and put <PrivyProvider> only inside privy-client-entry.tsx.
```tsx
// src/components/privy-root.tsx
import { lazy, Suspense, type ReactNode } from "react";
import { ClientOnly } from "@tanstack/react-router"; // or a { mounted } useEffect guard on plain Vite

const PrivyClientEntry = lazy(() => import("./privy-client-entry"));

export function PrivyRoot({ children }: { children: ReactNode }) {
  return (
    <ClientOnly fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
      <Suspense fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
        <PrivyClientEntry>{children}</PrivyClientEntry>
      </Suspense>
    </ClientOnly>
  );
}
```
- PrivyProvider config — pass bobSepolia as `defaultChain` + `supportedChains`.
  chainId is also passed per-call:
```tsx
// src/components/privy-client-entry.tsx
import { PrivyProvider } from "@privy-io/react-auth";
import { bobSepolia } from "@/lib/chains";

export default function PrivyClientEntry({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider
      appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{
        loginMethods: ["google", "email"],
        embeddedWallets: { ethereum: { createOnLogin: "users-without-wallets" } },
        defaultChain: bobSepolia,
        supportedChains: [bobSepolia],
        appearance: { theme: "dark" },
      }}
    >
      {children}
    </PrivyProvider>
  );
}
```
- Read the embedded wallet from useWallets, not user.wallet:
    const embedded = wallets.find(w => w.walletClientType === 'privy');
- Every send goes through Privy `useSendTransaction` with BOTH `address`
  and `sponsor`, wrapped in a 45s Promise.race timeout whose reject message
  names the exact dashboard toggles:
    await Promise.race([
      sendTransaction(
        { to, data, chainId: 808813 },
        { address: embedded.address, sponsor: true }
      ),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB Sepolia -> Allow transactions from the client."
      )), 45_000)),
    ]);
- Do NOT pass uiOptions:{ showWalletUIs:false } — it aborts with
  "signal is aborted without reason". The approval sheet still shows on
  the embedded-EOA path; the fee reads US$0.00.
- Do NOT add ZeroDev / SmartWalletsProvider / a paymaster URL. Native
  Privy sponsorship on BOB Sepolia works with the toggles above and nothing else.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
  -> App pays -> add custom chain "BOB Sepolia" (chainId 808813, rpc
  https://bob-sepolia.rpc.gobob.xyz) -> toggle "Allow transactions from
  the client" ON. Without this, sendTransaction hangs silently.

CONTRACT (contracts/SceneMoodChain.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SceneMoodChain
/// @notice Record color grading versions on blockchain to secure post-production creative decisions.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract SceneMoodChain {
    event Logged(address indexed author, string cid, uint256 at);
    /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
    function log(string calldata cid) external {
        emit Logged(msg.sender, cid, block.timestamp);
    }
}
```

DEPLOY + VERIFY
- Hardhat lives at the repo root with sources in /contracts (kept outside the
  Vite bundle). Install `@nomicfoundation/hardhat-toolbox` AND
  `@nomicfoundation/hardhat-verify@latest`.
- hardhat.config.cjs uses hardhat-verify's Blockscout customChain shape
  (BOB Sepolia Explorer is Blockscout-based, not Etherscan):
```js
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-verify");

module.exports = {
  solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
  networks: {
    bobSepolia: {
      url: process.env.BOB_SEPOLIA_RPC_URL || "https://bob-sepolia.rpc.gobob.xyz",
      accounts: [process.env.METAMASK_PRIVATE_KEY.startsWith("0x")
        ? process.env.METAMASK_PRIVATE_KEY : "0x" + process.env.METAMASK_PRIVATE_KEY],
      chainId: 808813,
    },
  },
  etherscan: {
    apiKey: { bobSepolia: "empty" }, // Blockscout ignores the key but the field is required
    customChains: [{
      network: "bobSepolia",
      chainId: 808813,
      urls: {
        apiURL: "https://bob-sepolia.explorer.gobob.xyz/api",
        browserURL: "https://bob-sepolia.explorer.gobob.xyz/",
      },
    }],
  },
  sourcify: { enabled: false },
};
```
```js
// scripts/deploy.cjs
const hre = require("hardhat");
const fs = require("fs");

async function main() {
  const f = await hre.ethers.getContractFactory("SceneMoodChain");
  const c = await f.deploy();
  await c.waitForDeployment();
  const address = await c.getAddress();
  console.log("deployed:", address);
  fs.writeFileSync("src/data/contract.json", JSON.stringify({
    address,
    deployTx: c.deploymentTransaction().hash,
    chainId: 808813,
    explorer: "https://bob-sepolia.explorer.gobob.xyz",
  }, null, 2));
}
main().catch((e) => { console.error(e); process.exit(1); });
```
- Deploy: `npx hardhat run scripts/deploy.cjs --network bobSepolia`.
- Verify (run RIGHT AFTER deploy, no constructor args for these contracts):
  `npx hardhat verify --network bobSepolia <address>`.
  On success Blockscout returns "Successfully verified contract" and the
  source becomes readable at
  `https://bob-sepolia.explorer.gobob.xyz/address/<address>#code`.
  Blockscout does NOT auto-verify — always run the command explicitly.
- Frontend reads: create a viem public client with the BOB Sepolia RPC —
  `createPublicClient({ chain: bobSepolia, transport: http(import.meta.env.VITE_BOB_SEPOLIA_RPC_URL ?? "https://bob-sepolia.rpc.gobob.xyz") })`.

FRONTEND CODE (src/pages/Index.tsx — adapt copy, keep the choreography)
```tsx
import { useState } from "react";
import { usePrivy, useWallets, useSendTransaction } from "@privy-io/react-auth";
import { createPublicClient, encodeFunctionData, http } from "viem";
import { bobSepolia, EXPLORER } from "@/lib/chains";
import { ABI } from "@/lib/contract";
import contract from "@/data/contract.json";

const client = createPublicClient({ chain: bobSepolia, transport: http() });

export default function Index() {
  const { ready, authenticated, login, logout } = usePrivy();
  const { wallets } = useWallets();
  const { sendTransaction } = useSendTransaction();
  const embedded = wallets.find((w) => w.walletClientType === "privy");

  const [input, setInput] = useState("");
  const [status, setStatus] = useState<"idle" | "pending" | "done" | "error">("idle");
  const [txHash, setTxHash] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  async function submit() {
    if (!embedded) return;
    setStatus("pending"); setError(null); setTxHash(null);
    try {
      const data = encodeFunctionData({ abi: ABI, functionName: "log", args: [input] });
      const res: any = await Promise.race([
        sendTransaction(
          { to: contract.address as `0x${string}`, data, chainId: 808813 },
          { address: embedded.address, sponsor: true }
        ),
        new Promise((_, r) => setTimeout(() => r(new Error(
          "Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB Sepolia -> Allow transactions from the client."
        )), 45_000)),
      ]);
      const hash = res?.hash ?? res;
      setTxHash(hash);
      await client.waitForTransactionReceipt({ hash });
      setStatus("done");
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
      setStatus("error");
    }
  }

  if (!ready) return <main className="p-10">Loading…</main>;

  return (
    <main className="min-h-screen mx-auto max-w-2xl px-6 py-16">
      <h1 className="text-4xl font-semibold tracking-tight">SceneMood Chain</h1>
      <p className="mt-3 opacity-70">Record color grading versions on blockchain to secure post-production creative decisions.</p>

      {!authenticated ? (
        <button onClick={login} className="mt-8 px-6 py-3 rounded-md bg-primary text-primary-foreground">
          Sign in with Google
        </button>
      ) : (
        <section className="mt-8 space-y-4">
          <div className="text-xs font-mono opacity-60">{embedded?.address}</div>
          <input value={input} onChange={(e) => setInput(e.target.value)}
                 placeholder="Describe the color grading proof entry…"
                 className="w-full rounded-md border px-4 py-3 bg-transparent" />
          <button onClick={submit} disabled={status === "pending" || !input}
                  className="px-6 py-3 rounded-md bg-primary text-primary-foreground disabled:opacity-40">
            {status === "pending" ? "Writing to BOB Sepolia…" : "Record onchain"}
          </button>
          {txHash && (
            <a href={`${EXPLORER}/tx/${txHash}`} target="_blank" rel="noreferrer"
               className="block text-sm underline break-all">
              {status === "done" ? "Confirmed" : "Pending"} · {txHash}
            </a>
          )}
          {error && <p className="text-sm text-red-500 break-all">{error}</p>}
          <button onClick={logout} className="text-xs uppercase tracking-widest opacity-60">Sign out</button>
        </section>
      )}

      <footer className="mt-20 text-xs opacity-60">Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14</footer>
    </main>
  );
}
```
Also export the ABI from src/lib/contract.ts so the page and any read call share it.

HOOK INTEGRATION — direct contract write
- One write function, one read. After the tx confirms, re-read the contract
  (or the emitted `Logged` event) and render the result from chain state — not
  from local component state — so a refresh shows the same thing.
```ts
const logs = await client.getContractEvents({
  address: contract.address as `0x${string}`,
  abi: ABI,
  eventName: "Logged",
  fromBlock: "earliest",
});
```
- Show a running feed of the last few entries with author, value and timestamp,
  each linking to `${EXPLORER}/tx/<hash>`.

USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned.
2. User performs a color grading proof action; the app calls `log(payload)` on the contract via Privy sponsored tx and shows the BOB Sepolia Explorer link as proof.
3. While the tx is in flight the button reads 'Writing to BOB Sepolia…' and the hash appears as a pending link; on receipt it flips to 'Confirmed'.
4. The page re-reads chain state so a refresh shows the same result.
5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

DESIGN DIRECTION
Commit to one distinctive look drawn from filmmaking & animation — specifically
color grading proof. 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-of-onchain elements (address, tx hash, CID) should read as
deliberate typographic detail in a mono face, not as debug output.

TROUBLESHOOTING (these are the failures you will actually hit)
- Blank page / "document is not defined" on load -> @privy-io/react-auth was
  imported at module scope of a route. Move it behind the lazy client entry.
- sendTransaction never resolves, no error -> the Privy dashboard gas-sponsorship
  toggle for BOB Sepolia is off. Add the custom chain and enable "Allow
  transactions from the client".
- "signal is aborted without reason" -> you passed uiOptions:{ showWalletUIs:false }.
  Remove it.
- Tx reverts with EMPTY revert data -> you encoded a function the deployed
  contract does not have. Check the verified ABI on Blockscout before
  debugging the UI.
- Tx sits pending forever -> underpriced. Resend the same nonce with a higher
  maxFeePerGas (~5 gwei) and 1 gwei priority fee.
- `hardhat verify` fails with "missing API key" -> keep the dummy
  `apiKey: { bobSepolia: "empty" }` entry; Blockscout ignores the value but
  hardhat-verify requires the field.
- Explorer shows "not verified" after deploy -> Blockscout never auto-verifies.
  Run the verify command.
- Reads return nothing -> you are pointed at the wrong chain. Confirm chainId
  808813 and the BOB Sepolia RPC, not Ethereum Sepolia (11155111).

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- METAMASK_PRIVATE_KEY  BOB Sepolia deployer key. Fund flow: get Sepolia ETH from https://sepolia-faucet.pk910.de/ then bridge to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- BOB_SEPOLIA_RPC_URL   HTTPS RPC (default: https://bob-sepolia.rpc.gobob.xyz). Public RPC works for hackathon load; swap for a dedicated node provider if you need higher rate limits.
- PRIVY_APP_ID          Google sign-in + sponsored tx. Docs: https://docs.privy.io/llms-full.txt
- PINATA_JWT            IPFS uploads (only if app pins media). Docs: https://docs.pinata.cloud/llms-full.txt

CREDIT (must appear in UI footer AND as NatSpec on every deployed contract):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Appendix · Market

Market sizing.

TAM
$400B
post-production market
SAM
$600M
color grading software
SOM
$12M
blockchain-based creative proofing

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

See also

Adjacent entries.