Poem Remix Repository
Host and pin collaborative poem remixes with full version history on decentralized storage.
The primitive.
Writers pick a handle and instantly receive <handle>.<parent>.eth — a gasless ENSIP-25 subname issued by a CCIP-Read gateway so poetry collaboration attribution works without ever paying gas.
Why this primitiveENSIP-25 lets Poem Remix Repository issue a namespaced handle to every poetry collaboration participant with zero gas — the parent name is the brand, each user gets a subname resolved via CCIP-Read from an offchain gateway.
Required keys.
Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
The build prompt.
Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →
Build "Poem Remix Repository" in ONE Lovable message. Single-page demo that mints
GASLESS offchain subnames under a parent `.eth` name using ENSIP-10 (wildcard)
via the Namestone managed gateway. No Solidity, no contract deploy — this is
the whole point of ENSIP-25: subnames without paying gas.
CONCEPT
Host and pin collaborative poem remixes with full version history on decentralized storage.
Discipline: Writing, Poetry & Narrative (poetry collaboration).
ENS primitive: ENSIP-25 offchain subnames via a CCIP-Read wildcard resolver (Namestone-managed).
Why this primitive: ENSIP-25 lets Poem Remix Repository issue a namespaced handle to every poetry collaboration participant with zero gas — the parent name is the brand, each user gets a subname resolved via CCIP-Read from an offchain gateway.
5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no auth
flows beyond Privy drop-in.
- No custom Solidity unless the hook explicitly deploys one — reuse the
canonical ENS / Durin / Namestone contracts listed below.
- Privy is always the auth + sponsored-tx layer (Google login, embedded
wallet, native gas sponsorship on Sepolia and Base Sepolia).
- At most ONE AI call per user action (Lovable AI Gateway + LOVABLE_API_KEY
if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.
ENS SPECIFICS — offchain subnames via Namestone:
Prerequisite (one-time, done via https://namestone.com dashboard):
- Own a parent `.eth` name on Sepolia (verify with
`ENSRegistry.owner(namehash(parent))` — never trust the app.ens.dev UI,
which shows "owned" after only the commit step).
- On sepolia.app.ens.domains, set the parent's resolver to Namestone's
offchain resolver (Namestone shows the exact address). The app must
verify this by calling `PublicResolver.supportsInterface(0x9061b923)`
(ENSIP-10 `resolve(bytes,bytes)`). If false, do NOT try to change it
programmatically — render a copy-paste panel telling the user to swap
the resolver at sepolia.app.ens.domains and reload.
- Enable the parent domain in the Namestone dashboard; grab NAMESTONE_API_KEY.
Mint a subname (from the browser — no wallet popup, truly gasless):
await fetch('https://namestone.com/api/public_v1/set-name', {
method: 'POST',
headers: { Authorization: import.meta.env.NAMESTONE_API_KEY,
'Content-Type': 'application/json' },
body: JSON.stringify({
domain: import.meta.env.VITE_PARENT_ENS_NAME, // e.g. "creative.eth"
name: label, // e.g. "alice"
address: embedded.address,
text_records: { description: "poetry collaboration — Writing, Poetry & Narrative" },
}),
});
Verify end-to-end (this MUST resolve within ~10s of the POST):
const resolved = await publicClient.getEnsAddress({
name: `${label}.${parent}`,
universalResolverAddress: undefined, // viem picks the L1 default
});
// resolved should equal embedded.address.
Pre-flight guards (render the "Mint subname" button DISABLED until true):
1. Parent ownership: `ENSRegistry.owner(namehash(parent)) !== 0x0`.
2. Parent resolver: `resolver(parent) === Namestone offchain resolver`.
3. Resolver supports ENSIP-10: `supportsInterface(0x9061b923)` returns true.
4. Label matches `/^[a-z0-9-]{1,63}$/` and is not already minted (call
Namestone `/api/public_v1/get-names?domain=&name=` first).
UNIVERSAL RESOLVER V2 (use this for EVERY name read — it is the only
reader that works across v1, ENSv2, offchain ENSIP-25 and Durin L2 names):
- Sepolia address: 0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe
- ABI subset:
resolve(bytes name, bytes data) view returns (bytes result, address resolver)
findCanonicalName(bytes name) view returns (bytes canonicalName, address resolver)
reverse(bytes lookupAddress, uint256 coinType) view returns (string name, address resolver, address reverseResolver)
- `name` is DNS-WIRE-ENCODED, not a string and not a namehash:
import { toHex, namehash } from 'viem';
import { packetToBytes } from 'viem/ens';
const wire = toHex(packetToBytes('alice.eth'));
- Always read through a viem PUBLIC CLIENT (CCIP-Read is on by default). Reading
through Privy's embedded provider throws `OffchainLookup` and breaks offchain
and L2 names.
- `ResolverNotFound` means "no resolver set", NOT "name unregistered" — show
different copy for each.
- Reverse records are opt-in; most Sepolia addresses have none. Fall back to the
truncated address, never "unknown".
- After every register/mint, verify with a Universal Resolver read and show the
resolved address in the success card. It is a free call.
PRIVY (SSR-safe mount + embedded-EOA routing — the four traps):
- Never import @privy-io/react-auth at module scope of a route file. Use
const Client = lazy(() => import('./privy-client-entry'));
inside <ClientOnly><Suspense>...</Suspense></ClientOnly>. Put
<PrivyProvider> only inside privy-client-entry.tsx.
- PrivyProvider config (do NOT stub defaultChain as { id, name } — pass
viem's `sepolia` or omit; 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' } }}>
- TRAP 1 — smart account vs EOA. `wallets[0]` and `useSendTransaction`
can pick an EIP-4337 smart account that is EMPTY even when your embedded
EOA is funded. Always pick the embedded EOA explicitly:
const embedded = wallets.find(w => w.walletClientType === 'privy');
const provider = await embedded.getEthereumProvider();
Display `embedded.address` (not smartAccount.address) as the fund-me box
and poll THAT balance.
- TRAP 2 — stale RPC after switchChain. Privy caches the provider's RPC
URL. After every switchChain, re-fetch AND assert:
await embedded.switchChain(11155111);
const p = await embedded.getEthereumProvider();
if ((await p.request({method:'eth_chainId'})) !== '0xaa36a7')
throw new Error('Provider still not on Sepolia — re-open Privy');
- TRAP 3 — `signal is aborted without reason`. Do NOT pass
uiOptions:{ showWalletUIs:false } (or any uiOptions) on the embedded
path. Send with ONLY { to, data, value, chainId }.
- TRAP 4 — sponsored tx hangs. Wrap every send in a 45s Promise.race
whose reject message names the exact dashboard toggle:
await Promise.race([
sendTransaction({ to, data, value, chainId: 11155111 },
{ address: embedded.address, sponsor: true }),
new Promise((_, r) => setTimeout(() => r(new Error(
"Privy sendTransaction timed out after 45s. In Privy dashboard: Gas sponsorship -> App pays -> add Ethereum Sepolia (and Base Sepolia if used) -> Allow transactions from the client."
)), 45_000)),
]);
- Do NOT add ZeroDev / SmartWalletsProvider / paymaster URL. Native
Privy sponsorship works with only the dashboard toggle above.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
-> App pays -> add "Ethereum Sepolia" (and "Base Sepolia" if the hook
uses it) -> toggle "Allow transactions from the client" ON.
STACK
- React + Vite single page. No router. No Hardhat, no Solidity.
- src/lib/namestone.ts: `mintSubname`, `checkAvailable`, `preflight`.
- viem public client on Sepolia for the four pre-flight reads.
- No sponsored tx needed for the mint itself (it's an API call), but the
Privy block above IS still required for the sign-in UX and for any
optional on-chain follow-ups (e.g. reverse record).
USER FLOW
1. Land -> "Sign in with Google" (Privy) -> embedded EOA auto-provisioned.
2. All four pre-flight checks show green (or a copy-paste fix panel).
3. Enter a label -> click "Claim subname" -> Namestone POST -> viem
resolves the new name to the user's EOA -> render success card with
`<label>.<parent>` and use it as the identity across the rest of the
poetry collaboration demo.
4. Footer renders the credit line below.
REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- SEPOLIA_RPC_URL (+ VITE_SEPOLIA_RPC_URL, same value) Alchemy Sepolia HTTPS endpoint. Create a free app at https://dashboard.alchemy.com/. Public RPCs throttle/fail — Alchemy required.
- PRIVY_APP_ID (+ VITE_PRIVY_APP_ID) Google sign-in + sponsored tx. Docs: https://docs.privy.io/llms-full.txt
- NAMESTONE_API_KEY Managed ENSIP-25 gateway API key. Sign up at https://namestone.com and enable your parent name.
CREDIT (must appear in UI footer):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Market sizing.
Indicative figures for hackathon pitches — refine with your own research before raising.