🎨 Visual Art · concept art progression

Storyboard Stamp

Mint sequential NFTs to authenticate evolving concept art storyboards and ideas.

NFT priced in USDC / EURC / cirBTC· multi-token mint
Section · Onchain

The primitive.

full primer →

Painters mint each concept art progression as an ERC-721 on Arc, choosing USDC, EURC or cirBTC at checkout, so authorship, price and timestamp are provable from a single arcscan link.

Why this primitiveERC-721 tokens create immutable chains of concept art evolution on the blockchain.

Kernel
an ERC-721 contract on Arc that mints a creator-owned token, accepting payment in USDC, EURC or cirBTC via a small on-contract token registry, then verified on the Arc explorer
Drives the UI as
a 'mint with your favourite stablecoin' button that returns tokenId, owner address and an arcscan link
Appendix · Secrets

Required keys.

CIRCLE_API_KEY
YOU PASTE. Backend Circle Console key — bootstrap uses it to mint everything else.
open ↗
PRIVY_APP_ID
YOU PASTE. Google sign-in + embedded Arc wallet (defaultChain = Arc 5042002). Expose via TanStack route loader — Lovable rejects VITE_* secret names.
open ↗
CIRCLE_ENTITY_SECRET
AUTO — `node scripts/bootstrap-circle.mjs` generates + registers it via Circle's RSA pubkey (POST, not PUT).
open ↗
CIRCLE_TREASURY_WALLET_ID
AUTO — bootstrap mints an ARC-TESTNET dev wallet. Then fund the address via the Circle Faucet.
open ↗
CIRCLE_TREASURY_ADDRESS
AUTO — the wallet address. NO VITE_ prefix (Lovable rejects it) — expose to the UI via a route loader that reads process.env on the server.
open ↗
CIRCLE_KIT_KEY
Optional. Circle App Kit client key for Bridge / Swap / balance widgets.
open ↗

Add these in your Lovable project under Settings → Secrets before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →

Build "Storyboard Stamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet.

CONCEPT
Mint sequential NFTs to authenticate evolving concept art storyboards and ideas.
Discipline: Visual Art (concept art progression).
Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens create immutable chains of concept art evolution on the blockchain.

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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan.
- Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002).
- USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens.
- At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

STACK
- React + Vite single page (the index route).
- Arc chain definition (viem `defineChain`, decimals: 6 — USDC is native gas):
```ts
// src/lib/arc-chain.ts
import { defineChain } from "viem";
// CRITICAL: nativeCurrency.decimals = 6 because USDC is the gas token on Arc.
// Using 18 (the default EVM assumption) corrupts every balance/fee display by 10^12.
// RPC hostname: pin `.network` — Circle docs interleave `.io` and `.network`; `.io` intermittently DNS-fails.
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,
});
```
- Privy embedded wallet. Wrap in `<ClientOnly>` + `React.lazy(() => import('./privy-root'))` —
  Workerd crashes on static import of `@privy-io/react-auth`. The provider config:
    import { PrivyProvider } from "@privy-io/react-auth";
    import { arcTestnet } from "@/lib/arc-chain";
    <PrivyProvider appId={privyAppId}
      config={{ loginMethods:['google'],
                 embeddedWallets:{createOnLogin:'users-without-wallets'},
                 defaultChain: arcTestnet as never,
                 supportedChains:[arcTestnet as never] }}>
  Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's
  chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader
  (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`.
- Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly:
```ts
// src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support
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;
```
- Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying.
  GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint.
  Never silently skip the `approve()` step; that looks like a bug.
- Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields
  bytecode that Arcscan cannot reproduce.
- Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`.

CONTRACT (contracts/StoryboardStamp.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title StoryboardStamp
/// @notice ERC-721 provenance for: Mint sequential NFTs to authenticate evolving concept art storyboards and ideas.
/// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract StoryboardStamp is ERC721 {
    uint256 public nextId;
    mapping(uint256 => string)  public cidOf;
    mapping(uint256 => address) public tokenOf;
    mapping(uint256 => uint256) public priceOf;
    address public immutable treasury;
    constructor(address _treasury) ERC721("StoryboardStamp", "STORYB") { treasury = _treasury; }
    /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
    function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) {
        require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay");
        id = ++nextId;
        cidOf[id]   = cid;
        tokenOf[id] = token;
        priceOf[id] = amount;
        _safeMint(msg.sender, id);
    }
    function tokenURI(uint256 id) public view override returns (string memory) {
        return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}"));
    }
}
```

BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy):
- Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here.
- Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`).
  It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints
  the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets.
- IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`.
  Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack
  route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as
  loader data. Same trick for `PRIVY_APP_ID`.
- The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET`
  is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`.
```js
// scripts/bootstrap-circle.mjs
// MANDATORY first step. Runs once in the Lovable sandbox. Idempotent.
// Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET,
// (2) create the ARC-TESTNET developer-controlled treasury wallet,
// (3) print all three values for the participant to paste into Project Settings -> Secrets.
import crypto from "node:crypto"; import fs from "node:fs";

const API = "https://api.circle.com/v1/w3s";
const KEY = process.env.CIRCLE_API_KEY;
if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); }
const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };

// 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new.
const RECOVERY = "circle-entity-recovery.json";
let entitySecret = process.env.CIRCLE_ENTITY_SECRET
                || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null)
                || crypto.randomBytes(32).toString("hex");
const provided = !!process.env.CIRCLE_ENTITY_SECRET;

// CRITICAL: persist BEFORE any network call. If registration succeeds and the
// process dies, the hex is unrecoverable — Circle never reveals it back.
// `wx` = fail if the file already exists, so an earlier secret is never clobbered.
try {
  fs.writeFileSync(RECOVERY, JSON.stringify({
    entitySecret, savedAt: new Date().toISOString(),
    warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.",
  }, null, 2), { flag: "wx" });
  console.log("Entity secret saved to", RECOVERY, "(pre-registration).");
} catch (e) {
  if (e.code !== "EEXIST") throw e;
  console.log(RECOVERY, "already exists — reusing it, not overwriting.");
}

// 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT).
const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json());
const ciphertext = crypto.publicEncrypt(
  { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
  Buffer.from(entitySecret, "hex")
).toString("base64");
const reg = await fetch(`${API}/config/entity/entitySecret`, {
  method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext })
});
if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) {
  console.error("register failed", reg.status, await reg.text()); process.exit(1);
}
if (reg.status === 409 && !provided) {
  console.error(`
409 = this Circle account ALREADY has an entity secret registered, and Circle never
reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes
the entity). Do NOT create more Circle accounts — they 409 too. Do this instead:

RELIABLE FIX — Circle Console configurator:
  1. Your 32-byte hex is already saved in ${RECOVERY}.
  2. Print the base64 ciphertext for it:
       node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})"
  3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext".
  4. Re-run: CIRCLE_ENTITY_SECRET=<the hex from ${RECOVERY}> node scripts/bootstrap-circle.mjs

If you already know the registered hex, skip straight to:
  CIRCLE_ENTITY_SECRET=<hex> node scripts/bootstrap-circle.mjs

NOTE: the recovery blob Circle hands back on registration is NOT the entity secret.
Only the 64-character hex goes into CIRCLE_ENTITY_SECRET.
`);
  process.exit(1);
}

// 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate.
const freshCipher = () => crypto.publicEncrypt(
  { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
  Buffer.from(entitySecret, "hex")
).toString("base64");
const setRes = await fetch(`${API}/developer/walletSets`, {
  method: "POST", headers: H,
  body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" })
}).then(r => r.json());
const walletSetId = setRes.data?.walletSet?.id;
if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); }
const walletRes = await fetch(`${API}/developer/wallets`, {
  method: "POST", headers: H,
  body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(),
                          walletSetId, blockchains: ["ARC-TESTNET"], count: 1, accountType: "EOA" })
}).then(r => r.json());
const wallet = walletRes.data?.wallets?.[0];
if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); }

console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n");
console.log("CIRCLE_ENTITY_SECRET      =", entitySecret);
console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id);
console.log("CIRCLE_TREASURY_ADDRESS   =", wallet.address, "  // NO VITE_ prefix — expose via route loader");
console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`);
console.log(`Address: ${wallet.address}`);
```

DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat):
- Install: `bun add solc@0.8.24 -d`
- Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/.
- Deploy script:
```js
// scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas).
// Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded.
// Usage: `node scripts/deploy-arc.mjs <ContractName> [ctorArg1 ctorArg2 ...]`
import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc";

const API = "https://api.circle.com/v1/w3s";
const KEY = process.env.CIRCLE_API_KEY;
const WID = process.env.CIRCLE_TREASURY_WALLET_ID;
const ES  = process.env.CIRCLE_ENTITY_SECRET;                    // 32-byte hex
if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); }
if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); }
const H = { Authorization: `Bearer ${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(ES, "hex")
  ).toString("base64");                                          // single-use — regen every call
}

function compile(name) {
  const source = fs.readFileSync(`contracts/${name}.sol`, "utf8");
  const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } },
    settings: { optimizer: { enabled: true, runs: 200 },
      outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } };
  const out = JSON.parse(solc.compile(JSON.stringify(input)));
  const c = out.contracts[`${name}.sol`][name];
  return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object };
}

const [, , name, ...ctorArgs] = process.argv;
const { abi, bytecode } = compile(name);

// Circle SCP payload shape that ACTUALLY works (learned the hard way):
// - idempotencyKey required
// - abiJson is a STRING (JSON.stringify), not an array
// - constructorParameters is positional
// - feeLevel is a flat string, NOT nested `fee: { type, config }`
const body = {
  idempotencyKey: crypto.randomUUID(),
  name,
  walletId: WID,
  blockchain: "ARC-TESTNET",
  abiJson: JSON.stringify(abi),
  bytecode,
  constructorParameters: ctorArgs,
  feeLevel: "MEDIUM",
  entitySecretCiphertext: await encryptEntitySecret(),
};
const deployRes = await fetch(`${API}/contracts/deploy`,
  { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json());
const contractId = deployRes.data?.contractId;
if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); }

// 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.mkdirSync("src/data", { recursive: true });
fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, chainId: 5042002, explorer: "https://testnet.arcscan.app" }, null, 2));
console.log(`Deployed ${name} -> ${addr} (https://testnet.arcscan.app/address/${addr})`);
```
- Run: `node scripts/deploy-arc.mjs StoryboardStamp`

VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening):
```js
// scripts/verify-arc.mjs — Blockscout standard-JSON verify. 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();

// Derive compiler version from the local solc so it EXACTLY matches deploy.
// Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce.
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.toString(), { method: "POST" });
console.log(res.status, await res.text());
// 200 with "Smart-contract verification started" means QUEUED, not verified.
// Poll GET https://testnet.arcscan.app/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min).
```
- Run: `node scripts/verify-arc.mjs contracts/StoryboardStamp.sol StoryboardStamp`
- HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll
  `GET https://testnet.arcscan.app/api/v2/smart-contracts/<addr>` until `is_verified: true` (~1-2 min).
- On success, source + ABI are readable at `https://testnet.arcscan.app/address/<address>?tab=contract`.

USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002.
2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the
   route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy
   button, and links to https://faucet.circle.com/ with the text
   "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload."
   If empty, render: "Run `node scripts/bootstrap-circle.mjs` first."
3. After the user creates a concept art progression, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/<hash>). CID can be an on-chain data URI.
4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button):
   "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234."
   Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts
   BLANK — that's not a bug. Your summary is what tells the user what they're actually signing.
5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

REQUIRED SECRETS

--- YOU PASTE (2 keys, that's it) ---
- CIRCLE_API_KEY            Circle Console API key (backend). https://console.circle.com/
- PRIVY_APP_ID              Google sign-in + embedded Arc wallet. https://dashboard.privy.io

--- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) ---
- CIRCLE_ENTITY_SECRET             32-byte hex, registered with Circle via RSA-OAEP.
- CIRCLE_TREASURY_WALLET_ID        Developer-controlled wallet on ARC-TESTNET.
- CIRCLE_TREASURY_ADDRESS          The wallet address. Exposed to the UI via a TanStack
                                   route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`.
                                   DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's
                                   secret store REJECTS any `VITE_*` prefix (reserved for the build).

--- OPTIONAL ---
- CIRCLE_KIT_KEY            Circle App Kit client key for Bridge / Swap widgets.
- LOVABLE_API_KEY           Only if the idea calls an LLM. Lovable AI Gateway.

NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet.

ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink)
- Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404.
- `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new
  API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to
  escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted
  locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste
  the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret
  Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=<hex>`.
- The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret.
  Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields
  `invalid entity secret` on every subsequent call.
- Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx`
  write flag so an existing secret is never silently overwritten. If registration succeeds and
  you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back.
- ONE entity secret per Circle account. A new API key on the same account inherits the same
  registered secret; it does not reset the write-once slot.
- Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet,
  deploy). Reusing a ciphertext is rejected.

GAS ON ARC (no paymaster needed)
USDC IS the native gas token on Arc Testnet — there is no ETH and no sponsorship policy to
configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will
not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet
and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain
or simply unfunded.

TROUBLESHOOTING (symptom -> cause -> fix)
- Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable
  reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose
  via a TanStack route loader reading `process.env.*` on the server.
- RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`.
- Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your
  own ciphertext through the Console configurator (see ENTITY-SECRET RULES).
- `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value.
- SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel`
  nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat.
- Deploy polls forever -> you're reading `data.contract.state` -> wait for
  `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`.
- Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma ->
  pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`,
  with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify.
- Wallet shows a wildly wrong native balance (off by 10^12) -> chain def defaulted to 18
  decimals -> `nativeCurrency.decimals: 6` in the viem chain.
- Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/.
- Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders
  BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc ->
  NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4).
- A token's `approve()` step appears to be skipped -> its configured address is
  `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve.

DEFINITION OF DONE (verify each one yourself)
1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address,
   and `circle-entity-recovery.json` exists on disk BEFORE any network call was made.
2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and
   cirBTC if the demo settles in them).
3. `node scripts/deploy-arc.mjs <Contract>` polls to `status: "COMPLETE"` and writes
   `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`.
4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and
   ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/<addr>` reports `is_verified: true`
   (Arcscan UI shows the green checkmark).
5. Google login via Privy provisions an embedded wallet on chain 5042002, and an
   approve + contract call confirms on Arcscan under that wallet's address.

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
Appendix · Market

Market sizing.

TAM
$65B
global visual art market
SAM
$350M
concept art services
SOM
$6M
provenance NFT storyboards

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.