🎵 Music & Sound Design · audio mastering records

Mastering Provenance Tags

Mint verifiable NFTs for mastered tracks to certify mastering sources and dates.

License Terms + Hook· programmable rights
Section · Onchain

The primitive.

full primer →

Musicians attach programmable License Terms — remix, commercial-use, royalty — the moment the audio mastering records is registered, and a DATA Foundation licensing hook enforces the rules onchain instead of in DMs.

Why this primitiveNFTs immutably link to mastered audio files and mastering engineer credentials.

Kernel
PIL License Terms attached at registration through `licenseTermsData`, optionally wired to a whitelisted DATA Foundation licensing hook (LockLicenseHook, TotalLicenseTokenLimitHook) so remix / commercial-use / royalty rules live onchain and enforce themselves
Drives the UI as
a rights sheet that renders the terms + hook in plain language and shows the licenseTermsId beside the IP Asset
Appendix · Secrets

Required keys.

PRIVY_APP_ID
Publishable — hardcode into src/config.ts. Google sign-in + Aeneid embedded wallet + sponsored tx.
open ↗
AENEID_RPC_URL
Publishable — hardcode into src/config.ts. Default: https://aeneid.storyrpc.io
open ↗

Hardcode both into src/config.ts (they're publishable, not secrets) before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure src/config.ts holds both values above. read the build strategy →

Build "Mastering Provenance Tags" in ONE Lovable message. Single-page demo on the DATA Foundation (Aeneid testnet).

CONCEPT
Mint verifiable NFTs for mastered tracks to certify mastering sources and dates.
Discipline: Music & Sound Design (audio mastering records).
On-chain primitive: License Terms + Hook (programmable rights). Why this primitive: NFTs immutably link to mastered audio files and mastering engineer credentials.
ipType: Song

5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no auth flows beyond Privy drop-in.
- Bespoke Solidity IS allowed but must stay tiny and sandbox-deployable: at most 1 contract file, ~120 LOC, no external deps beyond OpenZeppelin, no proxies, no upgradeability. Deploy it from the Lovable sandbox with Foundry (`forge script ... --rpc-url https://aeneid.storyrpc.io --broadcast --private-key $DEPLOYER_PK`) — never from the browser and never as part of the app runtime. Commit the resulting `{ address, chainId: 1315, abi }` into `src/data/<contract>.json` and import from there. For anything the SDKs already cover (register IP, license terms, mint license, CDR), keep using @story-protocol/core-sdk / @piplabs/cdr-sdk — do not re-implement those primitives in Solidity. Never redeploy Aeneid core contracts — they are already live.
- Privy is always the auth + sponsored-tx layer (Google login, embedded wallet).
- Off-chain storage: use IPFS only if the artefact must be public; use CDR (@piplabs/cdr-sdk) if it must be private.
- 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).
- Chain: DATA Foundation Aeneid testnet. chainId: 1315. Explorer: https://aeneid.explorer.datafdn.org  Block explorer: https://aeneid.datanetscan.io  Faucet: https://aeneid.faucet.datafdn.org
- SDKs (install with `bun add`):
    @story-protocol/core-sdk   // IP Assets, License Terms, License Tokens, Royalties
    @piplabs/cdr-sdk           // Confidential Data Rails (encrypt / gate / decrypt) — LAZY-LOAD only
    @privy-io/react-auth       // Google sign-in + embedded wallet + sponsored tx
    viem                        // walletClient wrapper around Privy embedded provider
- Custom contract path (optional — use only when the idea needs a bespoke event
  / registry / counter the SDKs don't cover; the demo runtime must still be a
  pure React + Vite single page):
    contracts/                     # foundry project committed alongside src/
      foundry.toml
      src/<Name>.sol               // your ~120 LOC contract, OpenZeppelin OK
      script/Deploy.s.sol          // forge script that broadcasts to Aeneid
  Deploy step (runs inside the Lovable sandbox — no local toolchain needed):
    cd contracts && forge build && \
      forge script script/Deploy.s.sol \
        --rpc-url https://aeneid.storyrpc.io \
        --broadcast --private-key $DEPLOYER_PK
  Fund the deployer once at https://aeneid.faucet.datafdn.org. Store
  DEPLOYER_PK as a Lovable Project Secret; never commit it. After deploy,
  write `{ address, chainId: 1315, abi }` to `src/data/<name>.json` and read
  it from the single-page app via viem `readContract` / `writeContract`.
  Reference: `contracts/ChoreoLedger.sol` + `contracts/script/Deploy.s.sol`
  + `src/data/choreo-ledger.json` in this repo.
- 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.
- SSR-safe CDR mount is mandatory. Never top-level-import @piplabs/cdr-sdk.
  Do `const cdr = await import('@piplabs/cdr-sdk')` inside the handler that
  actually needs to encrypt or decrypt. The bundled WASM must load on the
  client, not during SSR (breaks the published bundle otherwise).
- PrivyProvider config (do NOT stub defaultChain — chainId 1315 is passed per-call):
    import { PRIVY_APP_ID } from '@/config';
    <PrivyProvider appId={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');
- Bridge the Privy embedded wallet into a viem walletClient for the Story SDK:
    import { AENEID_RPC_URL } from '@/config';
    const provider = await embedded.getEthereumProvider();
    const walletClient = createWalletClient({
      account: embedded.address as `0x${string}`,
      chain: { id: 1315, name: 'Story Aeneid', nativeCurrency: { name:'IP', symbol:'IP', decimals:18 },
               rpcUrls:{ default:{ http:[AENEID_RPC_URL] } } },
      transport: custom(provider),
    });
- Instantiate the Story client (SDK auto-wires Aeneid contracts):
    const story = StoryClient.newClient({
      wallet: walletClient,
      transport: http(AENEID_RPC_URL),
      chainId: 'aeneid',
    });
- Every raw sendTransaction (rare — mostly the SDK handles it) goes through
  Privy `useSendTransaction` with BOTH `address` and `sponsor`, ALWAYS with
  an explicit gas fallback (Privy embedded smart wallets reject gas: 0), and
  wrapped in a 45s Promise.race timeout:
    const gas = await publicClient.estimateGas({ to, data, account: embedded.address })
      .then(g => g * 120n / 100n).catch(() => 300_000n);
    await Promise.race([
      sendTransaction(
        { to, data, chainId: 1315, gas },
        { address: embedded.address, sponsor: true }
      ),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> Story Aeneid -> Allow transactions from the client. If Aeneid sponsorship is unavailable in your workspace, drop `sponsor: true` and prefund the embedded wallet from https://aeneid.faucet.datafdn.org"
      )), 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 Aeneid works with the toggles below and nothing else.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
  -> App pays -> add "Story Aeneid" (chainId 1315) -> toggle "Allow
  transactions from the client" ON. Without this, sendTransaction hangs silently.
  Fallback: if your Privy workspace does not offer Aeneid sponsorship yet,
  drop `sponsor: true`, tell the user to hit the faucet
  (https://aeneid.faucet.datafdn.org) once, then re-run — the fallback branch
  in the timeout wrapper already handles this.

ON-CHAIN WRITE (audio mastering records artefact -> IP Asset + PIL License Terms + optional hook):
    const ipMetadata = story.ipAsset.generateIpMetadata({
      title: "Mastering Provenance Tags",
      description: "Mint verifiable NFTs for mastered tracks to certify mastering sources and dates.",
      createdAt: Math.floor(Date.now() / 1000).toString(),
      ipType: "Song",
      mediaUrl,
      mediaHash,         // sha256(await file.arrayBuffer()) as 0x-prefixed hex
      mediaType: file.type,
      image: previewUrl,
      imageHash: mediaHash,
      creators: [{ name: displayName, address: embedded.address, contributionPercent: 100 }],
      tags: ["audio mastering records", "Song"],
    });
    const { ipId, licenseTermsIds, txHash } = await story.ipAsset.registerIpAsset({
      nft: { type: 'mint', spgNftContract: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc' },
      ipMetadata,
      licenseTermsData: [{
        terms: {
          transferable: true,
          commercialUse: true,
          commercialRevShare: 5,          // 5% of downstream revenue back to the licensor
          derivativesAllowed: true,
          defaultMintingFee: 0n,
          currency: '0x1514000000000000000000000000000000000000', // WIP on Aeneid
        },
        licensingConfig: {
          isSet: true,
          mintingFee: 0n,
          // Optional: cap total license mints. Whitelisted hook address from
          //   https://docs.datafdn.org/developers/deployed-smart-contracts
          licensingHook: '0xaBAD364Bfa41230272b08f171E0Ca939bD600478', // TotalLicenseTokenLimitHook
          hookData: '0x',
          commercialRevShare: 5,
          disabled: false,
          expectMinimumGroupRewardShare: 0,
          expectGroupRewardPool: '0x0000000000000000000000000000000000000000',
        },
      }],
    });
    // If using TotalLicenseTokenLimitHook, cap the mints:
    await story.license.setMaxLicenseTokens({ ipId, licenseTermsId: licenseTermsIds[0], maxLicenseTokens: 1000 });

USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned on Aeneid.
2. Show a rights sheet: 'commercial use ✓ · 5% royalty · remixable · capped at 1000 licenses'. Render the licenseTermsId beside the IP Asset link. Explain in one plain-language line what the terms mean for a downstream audio mastering records creator.
3. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

CONFIG (hardcode these two publishable values; they are NOT secrets):
- Privy App ID          Google sign-in + Aeneid embedded wallet. Hardcode into src/config.ts as
                        `export const PRIVY_APP_ID = "<your-app-id>"`. Get it at https://dashboard.privy.io
                        Docs: https://docs.privy.io/llms-full.txt
- Aeneid RPC URL        DATA Foundation Aeneid HTTPS endpoint. Hardcode into src/config.ts as
                        `export const AENEID_RPC_URL = "https://aeneid.storyrpc.io"` (default works;
                        swap in your own provider under load).

Lovable's `VITE_*` prefix is reserved by the platform, so client-side keys go in
committed config files. Both values above are publishable — they show up in the
built JS bundle and are safe to commit.

OPTIONAL SECRET (Lovable -> Project Settings -> Secrets):
- LOVABLE_API_KEY       Only if the idea makes an AI call. Auto-provisioned when
                        you turn on Lovable AI.

CREDIT (must appear in UI footer AND inside every IP Asset's `description` or `tags`):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Appendix · Market

Market sizing.

TAM
$900M
audio mastering market
SAM
$250M
mastering studios
SOM
$40M
freelance mastering engineers

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

See also

Adjacent entries.