EffectChain Library
Pin and share effect preset libraries on IPFS for repeatable, consistent video stylizations.
IPFS via Pinata· decentralized storage
Section · Onchain
full primer →The primitive.
Every effect presets artefact is pinned to IPFS through Pinata; videographers get a permanent CID and a public gateway preview instead of a fragile cloud URL.
Why this primitivePinata stores immutable effect preset manifests easily distributed to creators.
Kernel
a Pinata JWT upload that pins images / JSON / manifests to IPFS and returns a permanent CID
Drives the UI as
a 'pinned to IPFS' chip with the CID and an ipfs.io gateway preview
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 ↗Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
Appendix · Mega-prompt
The build prompt.
budget · 1 message
Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →
Build "EffectChain Library" in ONE Lovable message. Single-page demo.
CONCEPT
Pin and share effect preset libraries on IPFS for repeatable, consistent video stylizations.
Discipline: Videography & Film (effect presets).
Onchain primitive: IPFS via Pinata. Why this primitive: Pinata stores immutable effect preset manifests easily distributed to creators.
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.
- src/lib/pinata.ts uploads via `fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', { method:'POST', headers:{ Authorization: `Bearer ${import.meta.env.VITE_PINATA_JWT}` }, body: fd })`.
- 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/EffectChainLibrary.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title CIDLogEffectChainLibrary
/// @notice Pin and share effect preset libraries on IPFS for repeatable, consistent video stylizations.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract CIDLogEffectChainLibrary {
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. On submit, pin the effect presets artefact to Pinata, then call `log(cid)` on the contract via Privy sponsored tx. Render the CID, IPFS gateway preview, and Worldscan tx link.
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
Market sizing.
TAM
$1.1B
video effects software market
SAM
$110M
effect preset marketplaces
SOM
$16M
freelance editors and content creators
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
version control
FrameSync Ledger
Track and manage video edits through IPFS to ensure lossless version history for creators.
content authenticationClipStamp Vault
Securely timestamp video clips onchain to verify original creation times and prevent plagiarism.
preproduction planningStoryboard Chain
Create IPFS-backed interactive storyboards that persistently link video plans and references.
color gradingColorGrade Provenance
Track and share editable color grading profiles via IPFS for collaborative and consistent grading.