Dialogue DAO
Mint dialogue NFTs for decentralized collaboration with verified authorship.
NFT provenance mint· onchain authorship
Section · Onchain
full primer →The primitive.
Writers mint each scriptwriting collaboration as an ERC-721 token on BOB Mainnet pointing at an IPFS CID, so authorship and timestamp are provable from a single BOB Explorer link.
Why this primitiveERC-721 tokens provide transparent proof for multi-writer dialogue ownership.
Kernel
an ERC-721 contract on BOB Sepolia that mints a creator-owned token pointing at an IPFS CID, verified on BOB Sepolia Explorer
Drives the UI as
a 'mint to claim authorship' button that returns the tokenId, owner address, and BOB Sepolia Explorer link
Required keys.
ROOTSTOCK_TESTNET_RPC_URL
Alchemy BOB Mainnet 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 "Dialogue DAO" in ONE Lovable message. Single-page demo. This prompt is self-contained: everything you need is below — do not ask follow-up questions.
CONCEPT
Mint dialogue NFTs for decentralized collaboration with verified authorship.
Discipline: Writing, Poetry & Narrative (scriptwriting collaboration).
Onchain primitive: NFT provenance mint. Why this primitive: ERC-721 tokens provide transparent proof for multi-writer dialogue ownership.
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 BOB Sepolia, verified on BOB Sepolia Explorer (Blockscout).
- 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.
NETWORK — BOB Sepolia (testnet, everything below is required verbatim)
- chainId 808813 (hex 0xc57cd). BOB is an OP-Stack L2 secured by Bitcoin.
- Gas token is ETH. Never BTC, and never the BOB ERC-20 token — holding BOB
tokens on Ethereum L1 does not pay for gas on the BOB network.
- RPC: https://bob-sepolia.rpc.gobob.xyz
- Explorer: https://bob-sepolia.explorer.gobob.xyz (Blockscout, not Etherscan)
- Funding the deployer: claim Sepolia ETH at https://sepolia-faucet.pk910.de/
then bridge it to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- Reference: https://docs.gobob.xyz/docs/user-hub/networks
- Create src/lib/chains.ts exactly like this:
```ts
import type { Chain } from "viem";
export const bobSepolia: Chain = {
id: 808813,
name: "BOB Sepolia",
nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
default: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
public: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
},
blockExplorers: {
default: { name: "BOB Sepolia Explorer", url: "https://bob-sepolia.explorer.gobob.xyz" },
},
testnet: true,
};
export const EXPLORER = "https://bob-sepolia.explorer.gobob.xyz";
```
FILE LAYOUT (create every one of these — nothing else)
```text
contracts/DialogueDAO.sol the contract source below
hardhat.config.cjs Blockscout verify config below
scripts/deploy.cjs deploy script below
src/lib/chains.ts bobSepolia viem Chain (above)
src/lib/contract.ts ABI + address helper, viem public client
src/data/contract.json { "address": "0x…", "deployTx": "0x…" } written after deploy
src/components/privy-root.tsx ClientOnly + Suspense + lazy wrapper
src/components/privy-client-entry.tsx the ONLY file importing @privy-io/react-auth
src/pages/Index.tsx the entire demo UI (single page)
```
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.
```tsx
// src/components/privy-root.tsx
import { lazy, Suspense, type ReactNode } from "react";
import { ClientOnly } from "@tanstack/react-router"; // or a { mounted } useEffect guard on plain Vite
const PrivyClientEntry = lazy(() => import("./privy-client-entry"));
export function PrivyRoot({ children }: { children: ReactNode }) {
return (
<ClientOnly fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
<Suspense fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
<PrivyClientEntry>{children}</PrivyClientEntry>
</Suspense>
</ClientOnly>
);
}
```
- PrivyProvider config — pass bobSepolia as `defaultChain` + `supportedChains`.
chainId is also passed per-call:
```tsx
// src/components/privy-client-entry.tsx
import { PrivyProvider } from "@privy-io/react-auth";
import { bobSepolia } from "@/lib/chains";
export default function PrivyClientEntry({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={import.meta.env.VITE_PRIVY_APP_ID}
config={{
loginMethods: ["google", "email"],
embeddedWallets: { ethereum: { createOnLogin: "users-without-wallets" } },
defaultChain: bobSepolia,
supportedChains: [bobSepolia],
appearance: { theme: "dark" },
}}
>
{children}
</PrivyProvider>
);
}
```
- 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: 808813 },
{ address: embedded.address, sponsor: true }
),
new Promise((_, r) => setTimeout(() => r(new Error(
"Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB 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. Native
Privy sponsorship on BOB Sepolia works with the toggles above and nothing else.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
-> App pays -> add custom chain "BOB Sepolia" (chainId 808813, rpc
https://bob-sepolia.rpc.gobob.xyz) -> 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 })`.
CONTRACT (contracts/DialogueDAO.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
/// @title DialogueDAO
/// @notice ERC-721 provenance for: Mint dialogue NFTs for decentralized collaboration with verified authorship.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract DialogueDAO is ERC721 {
uint256 public nextId;
mapping(uint256 => string) public cidOf;
constructor() ERC721("DialogueDAO", "DIALOG") {}
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
function mint(string calldata cid) external returns (uint256 id) {
id = ++nextId; cidOf[id] = cid; _safeMint(msg.sender, id);
}
function tokenURI(uint256 id) public view override returns (string memory) {
return string(abi.encodePacked("ipfs://", cidOf[id]));
}
}
```
DEPLOY + VERIFY
- Hardhat lives at the repo root with sources in /contracts (kept outside the
Vite bundle). Install `@nomicfoundation/hardhat-toolbox` AND
`@nomicfoundation/hardhat-verify@latest`.
- hardhat.config.cjs uses hardhat-verify's Blockscout customChain shape
(BOB Sepolia Explorer is Blockscout-based, not Etherscan):
```js
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-verify");
module.exports = {
solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
networks: {
bobSepolia: {
url: process.env.BOB_SEPOLIA_RPC_URL || "https://bob-sepolia.rpc.gobob.xyz",
accounts: [process.env.METAMASK_PRIVATE_KEY.startsWith("0x")
? process.env.METAMASK_PRIVATE_KEY : "0x" + process.env.METAMASK_PRIVATE_KEY],
chainId: 808813,
},
},
etherscan: {
apiKey: { bobSepolia: "empty" }, // Blockscout ignores the key but the field is required
customChains: [{
network: "bobSepolia",
chainId: 808813,
urls: {
apiURL: "https://bob-sepolia.explorer.gobob.xyz/api",
browserURL: "https://bob-sepolia.explorer.gobob.xyz/",
},
}],
},
sourcify: { enabled: false },
};
```
```js
// scripts/deploy.cjs
const hre = require("hardhat");
const fs = require("fs");
async function main() {
const f = await hre.ethers.getContractFactory("DialogueDAO");
const c = await f.deploy();
await c.waitForDeployment();
const address = await c.getAddress();
console.log("deployed:", address);
fs.writeFileSync("src/data/contract.json", JSON.stringify({
address,
deployTx: c.deploymentTransaction().hash,
chainId: 808813,
explorer: "https://bob-sepolia.explorer.gobob.xyz",
}, null, 2));
}
main().catch((e) => { console.error(e); process.exit(1); });
```
- Deploy: `npx hardhat run scripts/deploy.cjs --network bobSepolia`.
- Verify (run RIGHT AFTER deploy, no constructor args for these contracts):
`npx hardhat verify --network bobSepolia <address>`.
On success Blockscout returns "Successfully verified contract" and the
source becomes readable at
`https://bob-sepolia.explorer.gobob.xyz/address/<address>#code`.
Blockscout does NOT auto-verify — always run the command explicitly.
- Frontend reads: create a viem public client with the BOB Sepolia RPC —
`createPublicClient({ chain: bobSepolia, transport: http(import.meta.env.VITE_BOB_SEPOLIA_RPC_URL ?? "https://bob-sepolia.rpc.gobob.xyz") })`.
FRONTEND CODE (src/pages/Index.tsx — adapt copy, keep the choreography)
```tsx
import { useState } from "react";
import { usePrivy, useWallets, useSendTransaction } from "@privy-io/react-auth";
import { createPublicClient, encodeFunctionData, http } from "viem";
import { bobSepolia, EXPLORER } from "@/lib/chains";
import { ABI } from "@/lib/contract";
import contract from "@/data/contract.json";
const client = createPublicClient({ chain: bobSepolia, transport: http() });
export default function Index() {
const { ready, authenticated, login, logout } = usePrivy();
const { wallets } = useWallets();
const { sendTransaction } = useSendTransaction();
const embedded = wallets.find((w) => w.walletClientType === "privy");
const [input, setInput] = useState("");
const [status, setStatus] = useState<"idle" | "pending" | "done" | "error">("idle");
const [txHash, setTxHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function submit() {
if (!embedded) return;
setStatus("pending"); setError(null); setTxHash(null);
try {
const data = encodeFunctionData({ abi: ABI, functionName: "mint", args: [input] });
const res: any = await Promise.race([
sendTransaction(
{ to: contract.address as `0x${string}`, data, chainId: 808813 },
{ address: embedded.address, sponsor: true }
),
new Promise((_, r) => setTimeout(() => r(new Error(
"Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB Sepolia -> Allow transactions from the client."
)), 45_000)),
]);
const hash = res?.hash ?? res;
setTxHash(hash);
await client.waitForTransactionReceipt({ hash });
setStatus("done");
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
setStatus("error");
}
}
if (!ready) return <main className="p-10">Loading…</main>;
return (
<main className="min-h-screen mx-auto max-w-2xl px-6 py-16">
<h1 className="text-4xl font-semibold tracking-tight">Dialogue DAO</h1>
<p className="mt-3 opacity-70">Mint dialogue NFTs for decentralized collaboration with verified authorship.</p>
{!authenticated ? (
<button onClick={login} className="mt-8 px-6 py-3 rounded-md bg-primary text-primary-foreground">
Sign in with Google
</button>
) : (
<section className="mt-8 space-y-4">
<div className="text-xs font-mono opacity-60">{embedded?.address}</div>
<input value={input} onChange={(e) => setInput(e.target.value)}
placeholder="Describe the scriptwriting collaboration entry…"
className="w-full rounded-md border px-4 py-3 bg-transparent" />
<button onClick={submit} disabled={status === "pending" || !input}
className="px-6 py-3 rounded-md bg-primary text-primary-foreground disabled:opacity-40">
{status === "pending" ? "Writing to BOB Sepolia…" : "Mint provenance token"}
</button>
{txHash && (
<a href={`${EXPLORER}/tx/${txHash}`} target="_blank" rel="noreferrer"
className="block text-sm underline break-all">
{status === "done" ? "Confirmed" : "Pending"} · {txHash}
</a>
)}
{error && <p className="text-sm text-red-500 break-all">{error}</p>}
<button onClick={logout} className="text-xs uppercase tracking-widest opacity-60">Sign out</button>
</section>
)}
<footer className="mt-20 text-xs opacity-60">Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14</footer>
</main>
);
}
```
Also export the ABI from src/lib/contract.ts so the page and any read call share it.
HOOK INTEGRATION — IPFS via Pinata
```ts
// src/lib/pinata.ts
export async function pinFile(file: File): Promise<string> {
const fd = new FormData();
fd.append("file", file);
const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
method: "POST",
headers: { Authorization: `Bearer ${import.meta.env.VITE_PINATA_JWT}` },
body: fd,
});
if (!res.ok) throw new Error(`Pinata ${res.status}: ${await res.text()}`);
const { IpfsHash } = await res.json();
return IpfsHash as string;
}
export async function pinJson(json: unknown): Promise<string> {
const res = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", {
method: "POST",
headers: {
Authorization: `Bearer ${import.meta.env.VITE_PINATA_JWT}`,
"Content-Type": "application/json",
},
body: JSON.stringify(json),
});
if (!res.ok) throw new Error(`Pinata ${res.status}: ${await res.text()}`);
return (await res.json()).IpfsHash as string;
}
```
Pin FIRST, then send the onchain tx with the returned CID. Preview the pinned
asset at `https://gateway.pinata.cloud/ipfs/<cid>` and show the CID as a chip
next to the explorer link. Never send the file itself onchain.
HOOK INTEGRATION — ERC-721 provenance mint
- Pin the artefact with `pinFile`, pin a metadata JSON
(`{ name, description, image: "ipfs://<cid>" }`) with `pinJson`, then call
`mint(metadataCid)`.
- Read the new tokenId back from the receipt logs, or call `nextId()` after
confirmation.
- Render an owned-token strip: loop ids 1..nextId, call `ownerOf(id)` and skip
the ones the signed-in address does not own, showing `cidOf(id)` previews.
```ts
const nextId = await client.readContract({ address, abi: ABI, functionName: "nextId" });
const cid = await client.readContract({ address, abi: ABI, functionName: "cidOf", args: [id] });
```
- Link each token to `https://bob-sepolia.explorer.gobob.xyz/token/<address>/instance/<id>`.
USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned.
2. After the user creates a scriptwriting collaboration artefact, pin the file to IPFS via Pinata, then call `mint(cid)` on the deployed contract through Privy's sponsored transaction. Show tokenId, IPFS preview (`https://gateway.pinata.cloud/ipfs/<cid>`), and BOB Sepolia Explorer mint-tx link.
3. While the tx is in flight the button reads 'Writing to BOB Sepolia…' and the hash appears as a pending link; on receipt it flips to 'Confirmed'.
4. The page re-reads chain state so a refresh shows the same result.
5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"
DESIGN DIRECTION
Commit to one distinctive look drawn from writing, poetry & narrative — specifically
scriptwriting collaboration. No purple-on-white gradient hero, no Inter/Poppins default stack, no
generic three-card feature row. Pick a real typeface pairing, one accent colour
plus a near-monochrome base, and a layout with an obvious focal point. Define
colours as CSS variables/tokens; never hardcode text-white or bg-black. Dark by
default. The proof-of-onchain elements (address, tx hash, CID) should read as
deliberate typographic detail in a mono face, not as debug output.
TROUBLESHOOTING (these are the failures you will actually hit)
- Blank page / "document is not defined" on load -> @privy-io/react-auth was
imported at module scope of a route. Move it behind the lazy client entry.
- sendTransaction never resolves, no error -> the Privy dashboard gas-sponsorship
toggle for BOB Sepolia is off. Add the custom chain and enable "Allow
transactions from the client".
- "signal is aborted without reason" -> you passed uiOptions:{ showWalletUIs:false }.
Remove it.
- Tx reverts with EMPTY revert data -> you encoded a function the deployed
contract does not have. Check the verified ABI on Blockscout before
debugging the UI.
- Tx sits pending forever -> underpriced. Resend the same nonce with a higher
maxFeePerGas (~5 gwei) and 1 gwei priority fee.
- `hardhat verify` fails with "missing API key" -> keep the dummy
`apiKey: { bobSepolia: "empty" }` entry; Blockscout ignores the value but
hardhat-verify requires the field.
- Explorer shows "not verified" after deploy -> Blockscout never auto-verifies.
Run the verify command.
- Reads return nothing -> you are pointed at the wrong chain. Confirm chainId
808813 and the BOB Sepolia RPC, not Ethereum Sepolia (11155111).
REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- METAMASK_PRIVATE_KEY BOB Sepolia deployer key. Fund flow: get Sepolia ETH from https://sepolia-faucet.pk910.de/ then bridge to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- BOB_SEPOLIA_RPC_URL HTTPS RPC (default: https://bob-sepolia.rpc.gobob.xyz). Public RPC works for hackathon load; swap for a dedicated node provider if you need higher rate limits.
- 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.5B
writing tools market
SAM
$160M
collaborative scriptwriting users
SOM
$16M
NFT dialogue collaboration users
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
poetry archiving
Verse Vault
Securely mint and prove ownership of original poetry on-chain for authentic literary legacy.
interactive storytellingNarrative Nexus
Mint branching story nodes as NFTs to ensure unique creator control and provenance.
screenwriting rightsScript Stamp
Mint screenplay drafts as NFTs to certify original authorship and version history.
lyric poetryPoet’s Provenance
Create unique NFT tokens for lyric poems, certifying creators and preserving provenance.