CollaboShot Chain
Securely track multi-photographer project ownership with shared NFTs.
License Terms + Hook· programmable rights
Section · Onchain
full primer →The primitive.
Photographers attach programmable License Terms — remix, commercial-use, royalty — the moment the collaborative shoots is registered, and a DATA Foundation licensing hook enforces the rules onchain instead of in DMs.
Why this primitivethe terms of remix / commercial-use / royalty need to enforce themselves in code, not in DMs
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
Required keys.
PRIVY_APP_ID
Publishable — hardcode into src/config.ts. Google sign-in + Aeneid embedded wallet + sponsored tx.
open ↗Hardcode both into src/config.ts (they're publishable, not secrets) before pasting the prompt below.
Appendix · Mega-prompt
The build prompt.
budget · 1 message
Paste into a fresh Lovable project. Make sure src/config.ts holds both values above. read the build strategy →
Build "CollaboShot Chain" in ONE Lovable message. Single-page demo on the DATA Foundation (Aeneid testnet).
CONCEPT
Securely track multi-photographer project ownership with shared NFTs.
Discipline: Photography (collaborative shoots).
On-chain primitive: License Terms + Hook (programmable rights). Why this primitive: the terms of remix / commercial-use / royalty need to enforce themselves in code, not in DMs
ipType: Photograph
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 (collaborative shoots artefact -> IP Asset + PIL License Terms + optional hook):
const ipMetadata = story.ipAsset.generateIpMetadata({
title: "CollaboShot Chain",
description: "Securely track multi-photographer project ownership with shared NFTs.",
createdAt: Math.floor(Date.now() / 1000).toString(),
ipType: "Photograph",
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: ["collaborative shoots", "Photograph"],
});
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 collaborative shoots 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
Market sizing.
TAM
$2.4B
photo software market
SAM
$150M
collaborative photo projects
SOM
$15M
shared ownership verification
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
photo authenticity
TrueShot Ledger
Prove original photo ownership to combat unauthorized reuse and forgery.
photo editing historyEditTrace Chain
Track complete edit histories on-chain for transparent creative workflows.
fine art printsProPrint Certify
Certify fine art photo prints with tamper-proof digital provenance.
event photographySnapEvent Auth
Validate event photo ownership instantly with blockchain timestamps.