Onchain Photo Presets
Create, license, and verify photo editing presets with immutable smart contract records.
Register IP Asset· onchain identity
Section · Onchain
full primer →The primitive.
Every preset licensing artefact is registered as an IP Asset on the DATA Foundation (Aeneid); photographers get a canonical ipId, a dispute-provable creator record, and an explorer link that survives platform churn.
Why this primitiveSmart contracts prove preset originality and license transfers transparently.
Kernel
a call to @story-protocol/core-sdk `client.ipAsset.registerIpAsset` on DATA Foundation Aeneid (chainId 1315) that mints an SPG NFT and binds it to a new IP Account with full IPA metadata (title, mediaHash, creators, ipType)
Drives the UI as
a 'registered on the DATA Foundation' badge with the ipId, owner address and an aeneid.explorer.datafdn.org link
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 "Onchain Photo Presets" in ONE Lovable message. Single-page demo on the DATA Foundation (Aeneid testnet).
CONCEPT
Create, license, and verify photo editing presets with immutable smart contract records.
Discipline: Photography (preset licensing).
On-chain primitive: Register IP Asset (onchain identity). Why this primitive: Smart contracts prove preset originality and license transfers transparently.
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 (preset licensing artefact -> registered IP Asset on Aeneid):
const ipMetadata = story.ipAsset.generateIpMetadata({
title: "Onchain Photo Presets",
description: "Create, license, and verify photo editing presets with immutable smart contract records.",
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: ["preset licensing", "Photograph"],
});
const { ipId, txHash, tokenId } = await story.ipAsset.registerIpAsset({
nft: { type: 'mint', spgNftContract: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc' },
ipMetadata,
});
// Show the IP Asset on the DATA Foundation explorer:
// https://aeneid.explorer.datafdn.org/ipa/${ipId}
USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned on Aeneid.
2. After the user finalises a preset licensing artefact, register it as an IP Asset. Render a 'registered on the DATA Foundation' badge with the ipId, owner and an aeneid.explorer.datafdn.org link. Cache the ipId in local state — downstream steps (license terms, license mint, CDR gating) all reference it.
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
$1.5B
photo editing tools market
SAM
$300M
preset marketplaces
SOM
$30M
photographers buying presets
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
copyright registry
Immutable Photo Rights
Securely register photo ownership and licensing rights on-chain with immutable proof.
image authenticityOnchain Photo Proof
Embed verifiable metadata on the DATA Foundation to prove photo authenticity and timestamp creation.
royalty automationSmart Photo Royalties
Automate and transparently distribute photo royalties directly via DATA Foundation IP Assets.
collaborative editingCollaborative Edits Chain
Track photo edit histories and collaborators immutably on the DATA Foundation for transparent teamwork.