Palette Swap
Allow painters and illustrators to exchange and license color palettes with transparent, gasless transactions.
Privy social + sponsored tx· wallet UX
Section · Onchain
full primer →The primitive.
Painters sign in with Google through Privy — no seed phrase, no MetaMask popup — and their color exchange actions are sent as faucet-funded ETH transactions so they never see gas.
Why this primitivePrivy social plus faucet-funded transactions enable easy palette licensing with zero gas hurdles.
Kernel
Privy embedded wallet bootstrapped with PRIVY_APP_ID — Google sign-in plus faucet-funded transactions so users never see gas
Drives the UI as
a one-click 'Sign in with Google' that drops the user straight into the app with an onchain identity
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 "Palette Swap" in ONE Lovable message. Single-page demo. This prompt is self-contained: everything you need is below — do not ask follow-up questions.
CONCEPT
Allow painters and illustrators to exchange and license color palettes with transparent, gasless transactions.
Discipline: Visual Art (color exchange).
Onchain primitive: Privy social + sponsored tx. Why this primitive: Privy social plus faucet-funded transactions enable easy palette licensing with zero gas hurdles.
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/SocialLogPaletteSwap.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.
CONTRACT (contracts/SocialLogPaletteSwap.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SocialLogPaletteSwap
/// @notice Allow painters and illustrators to exchange and license color palettes with transparent, gasless transactions.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract SocialLogPaletteSwap {
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);
}
}
```
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("SocialLogPaletteSwap");
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: "log", 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">Palette Swap</h1>
<p className="mt-3 opacity-70">Allow painters and illustrators to exchange and license color palettes with transparent, gasless transactions.</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 color exchange 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…" : "Record onchain"}
</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 — Privy social login + sponsored tx
- Login methods are Google and email only. The embedded wallet is created on
login for users without one, so there is no seed phrase, no extension, and no
network-switching prompt.
- Show the user's Google avatar/email from `usePrivy().user` next to the short
wallet address so the onchain identity reads as a normal profile.
- Every write is sponsored: the approval sheet shows a US$0.00 fee. Say
"gas covered" in the UI rather than hiding the sheet.
- Keep a visible session state machine: signed out -> signing in -> ready ->
writing -> confirmed, and never leave a button in an ambiguous state.
USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned.
2. Every color exchange action the user performs is sent as a sponsored BOB Sepolia tx (`log(payload)`) and displayed with a BOB Sepolia Explorer link. No wallet popups.
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 visual art — specifically
color exchange. 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
$500M
digital color licensing market
SAM
$120M
palette trading platforms
SOM
$10M
early palette exchange adopters
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
collaborative painting
Canvas Collaborate
Enable painters to co-create artworks with seamless onchain identity and gasless contributions.
illustrator communityIllustrator Guild
Create a hosted network for illustrators to share, sell, and verify work with easy login and zero gas fees.
generative artGenerative Canvas
Let generative artists deploy and monetize code-driven art with instant gasless onboarding and transactions.
art provenance trackingGallery Ledger
Provide gallerists with gasless, onchain tools to prove artwork provenance and authenticity effortlessly.