🎭 Theater & Live Performance · royalty distribution

ScriptRoyalty Split

Automate fair payouts to playwrights, directors, and actors using onchain royalty splits.

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 directors can act on without web3 jargon.

Why this primitiveBOB Sepolia smart contracts ensure transparent, trustless revenue sharing from live performances.

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 "ScriptRoyalty Split" in ONE Lovable message. Single-page demo. This prompt is self-contained: everything you need is below — do not ask follow-up questions.

CONCEPT
Automate fair payouts to playwrights, directors, and actors using onchain royalty splits.
Discipline: Theater & Live Performance (royalty distribution).
Onchain primitive: BOB Sepolia smart contract. Why this primitive: BOB Sepolia smart contracts ensure transparent, trustless revenue sharing from live performances.

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/ScriptRoyaltySplit.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/ScriptRoyaltySplit.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title ScriptRoyaltySplit
/// @notice Automate fair payouts to playwrights, directors, and actors using onchain royalty splits.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract ScriptRoyaltySplit {
    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("ScriptRoyaltySplit");
  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">ScriptRoyalty Split</h1>
      <p className="mt-3 opacity-70">Automate fair payouts to playwrights, directors, and actors using onchain royalty splits.</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 royalty distribution 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 royalty distribution 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 theater & live performance — specifically
royalty distribution. 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
$10B
global live performance revenue
SAM
$2B
royalty management software market
SOM
$400M
theater companies seeking automated royalty solutions

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

See also

Adjacent entries.