Real onchain, five secrets, one build.
Every mega-prompt in this repo uses the same pattern, because it's the only pattern that lets a Lovable account ship a verifiable Arc Testnet demo in one shot.
Why Arc Testnet?
Arc is Circle's EVM-compatible L1 with sub-second finality where USDC is the native gas token (6 decimals, not 18). Contracts deploy via Circle's Smart Contract Platform — no MetaMask, no Hardhat, no funded EOA. Gas comes straight from the treasury wallet in USDC. Verified on arcscan (Blockscout) with a single standard-JSON POST (no API key). Payments settle in USDC, EURC or cirBTC via ordinary ERC-20 transfers. Judges try the demo without holding ETH.
The recipe
# 1. Add the secrets (Settings -> Secrets). NOTE: NO VITE_ prefixes — Lovable rejects them. CIRCLE_API_KEY=... # console.circle.com (you paste) PRIVY_APP_ID=... # dashboard.privy.io (you paste) # The three below are generated for you by scripts/bootstrap-circle.mjs CIRCLE_ENTITY_SECRET=... # auto — printed by the bootstrap script (32-byte hex) CIRCLE_TREASURY_WALLET_ID=... # auto CIRCLE_TREASURY_ADDRESS=0x... # auto — the address to fund. Expose via route loader, not VITE_ CIRCLE_KIT_KEY=... # optional, App Kit widgets # 2. Run the bootstrap once in the Lovable sandbox — it generates + registers # CIRCLE_ENTITY_SECRET (POST /entitySecret, not PUT) and mints an ARC-TESTNET # treasury wallet. Persists the hex to disk BEFORE the network call. node scripts/bootstrap-circle.mjs # Paste the three values it prints into Project Settings -> Secrets. # 3. Fund the printed treasury ADDRESS with USDC (gas), EURC, cirBTC: open https://faucet.circle.com/ # 4. Copy a mega-prompt from this repo into Lovable. One paste: # - scaffolds the React app on Arc (viem defineChain, decimals: 6) # - writes a Solidity contract (pragma 0.8.24 exact, credit in NatSpec) # - deploys via Circle SCP (USDC gas straight from the treasury) # - verifies on arcscan / Blockscout via standard-JSON (no plugin, no key) # - wires Privy Google sign-in via route loader (NO VITE_ secret names) # - ships a USDC / EURC / cirBTC token switcher # 5. Open the arcscan link. Your demo is provably onchain on Circle Arc.
1. The contract — credit baked in
Every Solidity file deployed from a Creative Blockchain prompt MUST carry the hackathon credit in NatSpec, so provenance lives onchain alongside the bytecode.
// contracts/Provenance.sol — hackathon credit in NatSpec, pragma pinned exactly
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24; // exact — NO caret. Must match the pinned solc@0.8.24.
/// @title Provenance
/// @notice Built during the Creative AI & Quantum Hackathon
/// @notice organised by StreetKode Fam during Indian Krump Festival 14
contract Provenance {
// token = USDC / EURC / cirBTC ERC-20 on Arc Testnet
event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at);
function log(address token, uint256 amount, string calldata cid) external {
emit Logged(msg.sender, token, amount, cid, block.timestamp);
}
}
2. Deploy via Circle SCP (USDC gas)
No MetaMask, no Hardhat. Compile with solc, submit to Circle's Smart Contract Platform, and pay gas in USDC straight from the treasury wallet. Entity secret ciphertext is single-use — regenerate every call.
// scripts/deploy-arc.mjs — Circle SCP deploy. USDC gas from the treasury wallet.
// No MetaMask. No Hardhat. Compile with solc@0.8.24, submit to Circle, poll for the address.
import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc";
const API = "https://api.circle.com/v1/w3s";
const H = { Authorization: `Bearer ${process.env.CIRCLE_API_KEY}`, "Content-Type": "application/json" };
async function encryptEntitySecret() {
const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json());
return crypto.publicEncrypt(
{ key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256",
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
Buffer.from(process.env.CIRCLE_ENTITY_SECRET, "hex")
).toString("base64"); // single-use — regenerate every call
}
const name = process.argv[2];
const src = fs.readFileSync(`contracts/${name}.sol`, "utf8");
const out = JSON.parse(solc.compile(JSON.stringify({
language: "Solidity", sources: { [`${name}.sol`]: { content: src } },
settings: { optimizer: { enabled: true, runs: 200 },
outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } },
})));
const c = out.contracts[`${name}.sol`][name];
// Payload shape that actually works: idempotencyKey, abiJson as STRING,
// positional constructorParameters, flat feeLevel. NOT nested fee.type/config.
const { data: { contractId } } = await fetch(`${API}/contracts/deploy`, {
method: "POST", headers: H, body: JSON.stringify({
idempotencyKey: crypto.randomUUID(),
name,
walletId: process.env.CIRCLE_TREASURY_WALLET_ID,
blockchain: "ARC-TESTNET",
abiJson: JSON.stringify(c.abi),
bytecode: "0x" + c.evm.bytecode.object,
constructorParameters: [],
feeLevel: "MEDIUM",
entitySecretCiphertext: await encryptEntitySecret(),
}),
}).then(r => r.json());
// Poll contract.status === "COMPLETE" (NOT .state).
let addr;
for (let i = 0; i < 60 && !addr; i++) {
await new Promise(r => setTimeout(r, 2000));
const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json());
if (contract.status === "COMPLETE") addr = contract.contractAddress;
}
fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi: c.abi }, null, 2));
console.log(`Deployed ${name} -> ${addr}`);
3. Verify on arcscan / Blockscout (REST)
One POST to /api/v2/smart-contracts/<addr>/verification/via/flattened-code. No API key, no Etherscan, no Hardhat plugin.
// scripts/verify-arc.mjs — Blockscout standard-JSON. NO API key. NO plugin. NO flattening.
import fs from "node:fs";
import solc from "solc";
const addr = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")).address;
const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance
const source = fs.readFileSync(sourcePath, "utf8");
const fileName = sourcePath.split("/").pop();
// Compiler version derived from LOCAL solc — pin solc@0.8.24 in devDeps so it matches deploy.
const compilerversion = "v" + solc.version().split(".Emscripten")[0];
const input = JSON.stringify({
language: "Solidity", sources: { [fileName]: { content: source } },
settings: { optimizer: { enabled: true, runs: 200 },
outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } },
});
const params = new URLSearchParams({
module: "contract", action: "verifysourcecode",
contractaddress: addr, contractname: `${fileName}:${contractName}`,
compilerversion, optimizationUsed: "1", runs: "200",
sourceCode: input, codeformat: "solidity-standard-json-input",
licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true",
});
const res = await fetch("https://testnet.arcscan.app/api?" + params, { method: "POST" });
console.log(res.status, await res.text());
// 200 "Smart-contract verification started" = QUEUED. Poll
// GET https://testnet.arcscan.app/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min).
4. Multi-stablecoin support (USDC · EURC · cirBTC)
// src/lib/tokens.ts — USDC · EURC · cirBTC on Arc Testnet
export const ARC_CHAIN_ID = 5042002;
export const ARC_RPC_URL = "https://rpc.testnet.arc.network"; // pin .network — .io intermittently DNS-fails
export const ARC_EXPLORER = "https://testnet.arcscan.app";
export const TOKENS = {
USDC: { symbol: "USDC", address: "0x3600000000000000000000000000000000000000", decimals: 6, label: "US Dollar (native gas)" },
EURC: { symbol: "EURC", address: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", decimals: 6, label: "Euro Coin" },
cirBTC: { symbol: "cirBTC", address: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF", decimals: 8, label: "Circle Wrapped BTC" },
} as const;
export type TokenKey = keyof typeof TOKENS;
5. Sign in with Google via Privy
// src/lib/arc-chain.ts + src/routes/index.tsx — viem chain + Privy on Arc via route loader
import { defineChain } from "viem";
// CRITICAL: decimals: 6 because USDC is the gas token on Arc. Using 18 corrupts every fee by 10^12.
export const arcTestnet = defineChain({
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 6 },
rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } },
blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } },
testnet: true,
});
// Lovable rejects VITE_* secret names. Expose Privy + treasury via a route loader.
// src/routes/index.tsx
export const Route = createFileRoute("/")({
loader: () => ({
privyAppId: process.env.PRIVY_APP_ID ?? "",
treasuryAddress: process.env.CIRCLE_TREASURY_ADDRESS ?? "",
}),
component: Home,
});
// Wrap Privy in <ClientOnly> + React.lazy — Workerd crashes on static import.
import { PrivyProvider } from "@privy-io/react-auth";
<PrivyProvider
appId={privyAppId}
config={{
loginMethods: ["google", "email"],
embeddedWallets: { createOnLogin: "users-without-wallets" },
defaultChain: arcTestnet as never, // 'as never' required — Privy's chain type is narrow
supportedChains: [arcTestnet as never], // BOTH required — omitting drops the chain silently
}}
>
<App />
</PrivyProvider>
Hackathon rules of thumb
- · One mega-prompt = one build message. Don't iterate the architecture, iterate the UI.
- · Always show the live arcscan link in the UI — that's your proof.
- · Support all three tokens (USDC · EURC · cirBTC). Gate zero-address tokens in the switcher — never silently skip
approve(). - · Render your own pre-confirmation summary above Privy's approve modal. Privy always shows the gas-token (USDC) balance and leaves EURC/cirBTC amounts blank — that's not a bug, but users think it is.
- · Never save a Lovable secret as
VITE_*— the store rejects it. Expose via a TanStack route loader that readsprocess.env. - · Pin
solc@0.8.24in devDependencies and use exactpragma solidity 0.8.24;— newer solc yields bytecode Arcscan can't reproduce. - · Add a "Built during the Creative AI & Quantum Hackathon — StreetKode Fam · Indian Krump Festival 14" line to your footer.
Troubleshooting
Every mega-prompt ships this table inline, so a build recovers itself. Here it is browsable.