🎵 Music & Sound Design · live performance

Live Set Royalty Split

Automatically distribute royalty shares from live streamed performances via smart contracts.

Sepolia smart contract· onchain logic
Section · Onchain

The primitive.

full primer →

Live performance gets a tiny Solidity contract deployed to World Chain Sepolia; musicians see a 'verified onchain' badge with the live contract address and a one-tap Worldscan link.

Why this primitiveOnchain automation reduces complexity in live earnings distribution.

Kernel
a Solidity contract deployed to World Chain Sepolia via MetaMask private key, then verified on Worldscan (Etherscan v2, chainid=4801)
Drives the UI as
a 'verified onchain' badge with the live contract address and a Worldscan link
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on World Chain Sepolia via the Alchemy faucet.
open ↗
SEPOLIA_RPC_URL
Alchemy World Chain Sepolia HTTPS endpoint. Create a free app → copy the HTTPS URL.
open ↗
ETHERSCAN_API_KEY
Required for npx hardhat verify after deploy.
open ↗
PRIVY_APP_ID
Enables Google sign-in and sponsored 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 "Live Set Royalty Split" in ONE Lovable message. Single-page demo.

CONCEPT
Automatically distribute royalty shares from live streamed performances via smart contracts.
Discipline: Music & Sound Design (live performance).
Onchain primitive: World Chain smart contract. Why this primitive: Onchain automation reduces complexity in live earnings distribution.

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 World Chain Sepolia, verified on Worldscan.
- 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.

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.
- PrivyProvider config (do NOT stub defaultChain as { id, name } — omit it
  or pass viem's `sepolia`; chainId is passed per-call):
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google','email'],
                embeddedWallets:{ ethereum:{ createOnLogin:'users-without-wallets' } },
                appearance:{ theme:'dark' } }}>
- 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: 4801 },
        { address: embedded.address, sponsor: true }
      ),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> World Chain 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.
- World ID v4 humanness gate is mandatory before any onchain write.
  Production `app_id`s no longer accept the classic `worldapp://` WalletConnect
  deep link, so the v2 `IDKitWidget` "Open World App" button silently does
  nothing on real devices. Use v4: `IDKitRequestWidget` + server-signed
  `rp_context` + a server-side verify proxy. Modern World ID 4.0 apps are
  Orb-backed — you MUST use `orbLegacy({ signal })` as the preset;
  `deviceLegacy` triggers `failed_by_host_app` and `handleVerify` never fires.
  PORTAL SETUP (do this in https://developer.world.org before coding):
    1. Create/open the app -> copy `app_id` (public, ships to browser).
    2. Actions tab -> create an action slug (e.g. `log-action`). Verification
       level: Orb (World ID 4.0 default).
    3. Relying Parties tab (or "World ID 4.0" tab) -> create an RP. Copy the
       `rp_id` (public, `rp_...`) AND the signing key (hex private key, shown
       ONCE).
    4. Allow legacy proofs: World ID 4.0 apps have this implicitly enabled;
       the widget still needs `allow_legacy_proofs={true}`. Only revisit if
       verify returns `invalid_proof` while the client flag is set.
  PACKAGE: `bun add @worldcoin/idkit` (v4+; re-exports
  `@worldcoin/idkit-core/signing`). Do NOT install v2.
  FILE LAYOUT (5 files):
    - src/config/worldid.ts — PUBLIC ONLY. `{ appId: 'app_...' as `app_${string}`,
      action: 'log-action' }`. Never put rp_id or signing key here; anything
      under src/config/* ships to the browser.
    - src/components/worldid-gate.tsx — trigger button + `open` state. Lazy-load
      the inner widget:
        const IDKitInner = lazy(() => import('./worldid-inner'));
        // <ClientOnly fallback={null}><Suspense><IDKitInner open={open} .../></Suspense></ClientOnly>
    - src/components/worldid-inner.tsx — the actual widget. FREEZE the wallet
      address into a ref/state the moment the widget opens; use that frozen
      value in `orbLegacy({ signal })` AND in the verify POST body. Privy/wagmi
      can re-hydrate mid-flow and mutate `signal`, producing a valid-looking
      proof that server-verifies against the wrong signal.
      On `open` -> POST `/api/public/idkit/rp-signature` with `{ action }`,
      store the returned `{ rp_id, nonce, created_at, expires_at, signature }`
      as `rp_context`, then render:
        import { IDKitRequestWidget, orbLegacy } from '@worldcoin/idkit';
        const frozenSignal = useRef(''); // set in useEffect when open becomes true
        <IDKitRequestWidget open={open} onOpenChange={onOpenChange}
          app_id={appId} action={action} rp_context={rp}
          allow_legacy_proofs environment="production"
          preset={orbLegacy({ signal: frozenSignal.current })}
          handleVerify={async (result) => {
            const res = await fetch('/api/public/idkit/verify', {
              method:'POST', headers:{'Content-Type':'application/json'},
              body: JSON.stringify({
                rp_id: rp.rp_id, app_id: appId, idkitResponse: result,
              }),
            });
            if (!res.ok) {
              const j = await res.json().catch(() => ({}));
              throw new Error(`verify ${res.status}: ${j.detail ?? j.code}`);
            }
          }}
          onSuccess={onVerified} />
    - src/routes/api/public/idkit/rp-signature.ts — TSS server route. Reads
      `process.env.WORLDID_RP_ID` and `process.env.WORLDID_RP_SIGNING_KEY`
      INSIDE the handler (never at module scope), then:
        import { signRequest } from '@worldcoin/idkit-core/signing';
        const sig = signRequest({ signingKeyHex: signingKey, action, ttl: 300 });
        return Response.json({ rp_id: rpId, nonce: sig.nonce,
          created_at: sig.createdAt, expires_at: sig.expiresAt,
          signature: sig.sig });
    - src/routes/api/public/idkit/verify.ts — TSS server route. Reads the
      POST body `{ rp_id, app_id, idkitResponse }`, then UNWRAPS and forwards
      `idkitResponse` (top-level proof fields — nullifier_hash, merkle_root,
      proof, verification_level, action, signal_hash) to the CURRENT v4 host
      `https://developer.world.org/api/v4/verify/${rp_id}` with explicit
      Accept + User-Agent headers (the World edge returns text/html 403s
      without them). On a non-JSON 403 from the rp_id path, retry ONCE using
      `app_id` before giving up. Return diagnostic metadata (upstreamStatus,
      upstreamContentType, verifyIdSource, fallbackTried) so the UI can show
      a safe debug blob:
        const payload = body.idkitResponse ?? body;
        async function post(id) {
          const url = `https://developer.world.org/api/v4/verify/${id}`;
          const r = await fetch(url, {
            method:'POST',
            headers:{ 'Accept':'application/json',
                      'Content-Type':'application/json',
                      'User-Agent':'yourapp-worldid/1.0' },
            body: JSON.stringify(payload),
          });
          const text = await r.text();
          const ct = r.headers.get('content-type') ?? '';
          let parsed = null; try { parsed = JSON.parse(text); } catch {}
          return { r, text, ct, parsed, isJson: !!parsed || ct.includes('json') };
        }
        let a = await post(rpId); let fallbackTried = false;
        if (a.r.status === 403 && !a.isJson && appId && appId !== rpId) {
          fallbackTried = true;
          const b = await post(appId); if (b.r.ok) a = b;
        }
      Wrapping the proof under `{ rp_id, idkitResponse }` in the upstream POST
      returns `invalid_proof` immediately after World App shows "successful".
  UI RULES (avoid the "same issue" loop):
    * Separate "proof received" from "server verified" state. On backend
      failure, KEEP the proof-received state — do NOT bounce the user back to
      "Connect World App". Show the server error inline.
    * Add a "Copy debug" button that dumps a SAFE non-secret JSON blob:
      { appId (prefix...suffix), action, rpId (prefix...suffix),
        protocolVersion, environment, responseIdentifiers, upstreamStatus,
        upstreamHost, upstreamContentType, fallbackTried, verifyIdSource }.
    * On success show an explicit "Verified human" line; collapse the debug
      JSON behind a <details>. If the debug shows upstreamStatus 200 +
      application/json + verifyIdSource "body.rp_id" the flow IS working —
      the UI just needs to celebrate it.
  RULES:
    * `/api/public/*` prefix is mandatory — bypasses Lovable's published-site
      auth so World App can reach the endpoints cross-origin.
    * `<ClientOnly>` + lazy import is mandatory — IDKit references `window` at
      import time; a top-level import in a route file crashes SSR with
      `window is not defined`.
    * Fetch a fresh rp_signature on every `open` (5-min TTL); caching across
      the widget lifetime causes stale-nonce failures.
    * Read `process.env.*` INSIDE handlers, never at module scope of route
      files — Cloudflare Workers inject env per-request.
    * `handleVerify` MUST throw on non-OK so IDKit fires `onError` and skips
      `onSuccess`. Swallowing the error shows a false-positive success.
    * Signing key stays server-side only. Never place it in src/config/*, any
      *.functions.ts module, or any file the router entry imports.
  VERIFIED-WORKING SHAPE (target this):
    preset=orbLegacy, protocol_version="3.0", responses[0].identifier="orb",
    upstream POST https://developer.world.org/api/v4/verify/${rp_id},
    upstreamStatus=200, upstreamContentType="application/json",
    verifyIdSource="body.rp_id", fallbackTried=false.
  FAILURE -> DIAGNOSIS:
    * `onError { errorCode: "failed_by_host_app" }` and `/verify` never called
      -> using `deviceLegacy` on an Orb-backed World ID 4.0 app. Switch preset
      to `orbLegacy({ signal })`.
    * Upstream returns 403 with `text/html` body -> World edge blocked the
      request shape. Add `Accept: application/json` + `User-Agent`, then fall
      back to `app_id` on non-JSON 403.
    * Proof succeeds once but UI re-mounts to "Connect World App" -> failure
      branch cleared the proof-received state. Keep it; only reset on explicit
      "Re-verify".
    * Signal-mismatch verify failure right after wallet auth -> `signal`
      mutated between widget open and handleVerify. Freeze it in a ref.
    * "Open World App" silent on prod -> using v2 widget with production
      app_id. Switch to v4 `IDKitRequestWidget` as above.
    * `rp-signature 500 no_rp_config` -> `WORLDID_RP_ID` or
      `WORLDID_RP_SIGNING_KEY` missing. Add via Lovable secrets, redeploy.
    * `verify 400 invalid_proof` right after World App shows "successful" ->
      server is forwarding `{ rp_id, idkitResponse }` wrapper instead of the
      unwrapped `idkitResponse` proof. Fix per rule above.
    * `verify 400 action_not_found` -> action string doesn't match the one
      created in the portal. Match verbatim (case-sensitive).
    * Build crashes with `window is not defined` -> IDKit imported outside
      `<ClientOnly>`. Move the import into the lazy `worldid-inner.tsx`.
    * User reports "same issue" but debug shows upstreamStatus 200 JSON with
      fallbackTried=false -> it IS working; the UI never displayed a clear
      success state. Add an explicit "Verified human" line and collapse debug.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
  -> App pays -> add "World Chain Sepolia" -> toggle "Allow transactions
  from the client" ON. Without this, sendTransaction hangs silently.
- Hardhat in /contracts (kept outside the Vite bundle). Install
  `@nomicfoundation/hardhat-toolbox` AND `@nomicfoundation/hardhat-verify@latest`
  (>=3.x — older versions still hit Worldscan v1 and fail with
  "You are using a deprecated V1 endpoint, switch to Worldscan V2 (chainid=4801)").
- hardhat.config.cjs MUST use the Worldscan v2 (chainid=4801) single-key shape (NOT the per-network map):
    require("@nomicfoundation/hardhat-toolbox");
    require("@nomicfoundation/hardhat-verify");
    module.exports = {
      solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
      networks: { worldchainSepolia: {
        url: process.env.SEPOLIA_RPC_URL || "https://worldchain-sepolia.g.alchemy.com/public",
        accounts: [process.env.METAMASK_PRIVATE_KEY.startsWith("0x")
          ? process.env.METAMASK_PRIVATE_KEY : "0x" + process.env.METAMASK_PRIVATE_KEY],
        chainId: 4801,
      } },
      etherscan: { apiKey: process.env.ETHERSCAN_API_KEY },   // single string, NOT { sepolia: ... }
      sourcify: { enabled: false },                            // silences the v2.x prompt
    };
- Deploy: `npx hardhat run scripts/deploy.cjs --network worldchainSepolia`.
- Verify (run RIGHT AFTER deploy, no constructor args for these contracts):
  `npx hardhat verify --network worldchainSepolia <address>`
  On success Worldscan returns "Successfully verified contract … on the block explorer"
  and the source becomes readable at
  `https://sepolia.worldscan.org/address/<address>#code`.
- Write the deployed address to `src/data/contract.json` so the UI links to
  `https://sepolia.worldscan.org/address/<address>`.

CONTRACT (contracts/LiveSetRoyaltySplit.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title LiveSetRoyaltySplit
/// @notice Automatically distribute royalty shares from live streamed performances via smart contracts.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract LiveSetRoyaltySplit {
    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);
    }
}
```

USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned.
2. User performs a live performance action; the app calls `log(payload)` on the contract via Privy sponsored tx and shows the Worldscan link as proof.
3. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- METAMASK_PRIVATE_KEY  World Chain Sepolia deployer key. Fund it: https://www.alchemy.com/faucets/world-chain-sepolia
- SEPOLIA_RPC_URL       Alchemy World Chain Sepolia HTTPS endpoint (https://eth-sepolia.g.alchemy.com/v2/<key>). Create a free app at https://dashboard.alchemy.com/ -> copy the HTTPS URL. Public RPCs throttle/fail under hackathon load — Alchemy is required.
- ETHERSCAN_API_KEY     For `npx hardhat verify`. Get: https://etherscan.io/myapikey
- 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
$4B
live music streaming economy
SAM
$1B
royalty management for live artists
SOM
$100M
early live performer adopters

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

See also

Adjacent entries.