Poetry Event Ledger
Pin event schedules, recordings and poems from live readings for lasting access.
CDR encrypted vault· confidential data
Section · Onchain
full primer →The primitive.
The live poetry events is sealed in a Confidential Data Rails vault against the validator DKG key; only writers holding the gating License Token can trigger threshold decryption and reconstruct the artefact client-side.
Why this primitivethe underlying artefact is sensitive and must stay encrypted at rest but programmatically unlockable
Kernel
Confidential Data Rails via @piplabs/cdr-sdk — an Uploader allocates a vault, TDH2-encrypts the artefact against the DKG public key with `uuidToLabel(uuid)` binding, and the on-chain condition gates threshold decryption on holding the License Token minted above
Drives the UI as
a 'sealed on Aeneid CDR' chip with the vault uuid; consumers with the License Token see a 'decrypt' action that stitches partials client-side
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 "Poetry Event Ledger" in ONE Lovable message. Single-page demo on the DATA Foundation (Aeneid testnet).
CONCEPT
Pin event schedules, recordings and poems from live readings for lasting access.
Discipline: Writing, Poetry & Narrative (live poetry events).
On-chain primitive: CDR encrypted vault (confidential data). Why this primitive: the underlying artefact is sensitive and must stay encrypted at rest but programmatically unlockable
ipType: Chapter
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 (live poetry events artefact -> encrypted CDR vault gated by a License Token):
// Lazy-load the CDR SDK inside the handler (top-level import breaks SSR + bundle):
const cdr = await import('@piplabs/cdr-sdk');
const cdrClient = cdr.CDRClient.forChain('aeneid', { walletClient });
// 1. Allocate a vault whose read condition is "holder of License Token X":
const vault = await cdrClient.uploader.allocate({
readCondition: cdr.conditions.holdsLicenseToken({ ipId, licenseTermsId }),
});
// 2. Encrypt the artefact bytes; the label MUST equal uuidToLabel(vault.uuid):
const ciphertext = await cdrClient.uploader.encrypt({
uuid: vault.uuid,
plaintext: new Uint8Array(await file.arrayBuffer()),
});
// 3. Write ciphertext + condition to CDR; validators enforce threshold decryption:
const { txHash } = await cdrClient.uploader.write({ vault, ciphertext });
// ALSO register a public-metadata-only IP Asset so the vault is discoverable:
const ipMetadata = story.ipAsset.generateIpMetadata({
title: "Poetry Event Ledger",
description: "Pin event schedules, recordings and poems from live readings for lasting access.",
createdAt: Math.floor(Date.now() / 1000).toString(),
ipType: "Chapter",
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: ["live poetry events", "Chapter"],
});
const { ipId } = await story.ipAsset.registerIpAsset({
nft: { type: 'mint', spgNftContract: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc' },
ipMetadata: { ...ipMetadata, cdrVaultUuid: vault.uuid, mediaUrl: undefined, mediaHash: undefined },
});
USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned on Aeneid.
2. Show a 'sealed on Aeneid CDR' chip with the vault uuid. Consumers who hold the gating License Token see a 'decrypt' action that calls `cdrClient.consumer.collectPartials(vault.uuid)` then `cdrClient.consumer.decrypt()` — the plaintext is reconstructed client-side and never touches your server.
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
$130M
poetry event and digital archiving market
SAM
$35M
live reading streaming and archive platforms
SOM
$6M
poetry event organizers and audiences
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
poetry archiving
Poetic Memory Vault
Permanently preserve poets' drafts and revisions for authentic literary heritage.
interactive storytellingNarrative Flow Sync
Save and share branching story manifests guaranteeing permanent access and collaboration.
character developmentCharacter Bios Hub
Store and distribute rich, immutable character profiles with multimedia attachments.
poetical imageryVerse Visualizer
Upload and preserve poetic image metaphors linked to verse for lasting inspiration.