🎨 Visual Art · physical palette digitization

Provenance Palette

Digitize and mint physical palette images as NFTs preserving unique artist color mixes.

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

The primitive.

full primer →

Painters mint each physical palette digitization 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 primitiveImmutable NFT mint authenticates physical palette provenance via IPFS hashes.

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).
open ↗
CIRCLE_ENTITY_SECRET
AUTO — `node scripts/bootstrap-circle.mjs` generates + registers it via Circle's RSA pubkey.
open ↗
CIRCLE_TREASURY_WALLET_ID
AUTO — bootstrap mints an ARC-TESTNET dev wallet. Then fund the address via the Circle Faucet.
open ↗
VITE_CIRCLE_TREASURY_ADDRESS
AUTO — the wallet address, shown in the UI faucet card with a copy button.
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 "Provenance Palette" in ONE Lovable message. Single-page demo on Circle's Arc Testnet.

CONCEPT
Digitize and mint physical palette images as NFTs preserving unique artist color mixes.
Discipline: Visual Art (physical palette digitization).
Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFT mint authenticates physical palette provenance via IPFS hashes.

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) will corrupt every balance/fee display by 10^12.
export const arcTestnet = defineChain({
  id: 5042002,
  name: "Arc Testnet",
  network: "arc-testnet",
  nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 6 },
  rpcUrls: { default: { http: [import.meta.env.VITE_ARC_RPC_URL ?? "https://rpc.testnet.arc.network"] } },
  blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } },
});
```
- Privy embedded wallet wraps `<App />` in src/main.tsx:
    import { PrivyProvider } from "@privy-io/react-auth";
    import { arcTestnet } from "@/lib/arc-chain";
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google'],
                 embeddedWallets:{createOnLogin:'users-without-wallets'},
                 defaultChain: arcTestnet,
                 supportedChains:[arcTestnet] }}>
  (Both `defaultChain` AND `supportedChains` are required — omitting `supportedChains` silently drops the custom chain.)
- 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  = import.meta.env.VITE_ARC_RPC_URL ?? "https://rpc.testnet.arc.network";
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: import.meta.env.VITE_CIRBTC_ADDRESS ?? "0x0000000000000000000000000000000000000000", 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.
- Circle App Kit (optional): `@circle-fin/kit` with `import.meta.env.VITE_CIRCLE_KIT_KEY` for Bridge / Swap widgets.
- Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`.
- Expose `PRIVY_APP_ID` as `VITE_PRIVY_APP_ID` and `CIRCLE_KIT_KEY` as `VITE_CIRCLE_KIT_KEY` (Vite only exposes VITE_-prefixed env to the client).

CONTRACT (contracts/ProvenancePalette.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 ProvenancePalette
/// @notice ERC-721 provenance for: Digitize and mint physical palette images as NFTs preserving unique artist color mixes.
/// @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 ProvenancePalette 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("ProvenancePalette", "PROVEN") { 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):
- The participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. Everything else 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) for the participant to paste into Project Settings -> Secrets.
- 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. Reuse an existing recovery file if present (re-runs after a remix).
const RECOVERY = "circle-entity-recovery.json";
let entitySecret = fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret
                                           : crypto.randomBytes(32).toString("hex");

// 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (409 = already registered, OK).
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: "PUT", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext })
});
if (reg.status !== 200 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); }
fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, warning: "Keep safe. Circle never reveals this again." }, null, 2));

// 3. Create treasury wallet on ARC-TESTNET (or reuse via wallet-set list on re-run).
const idempotencyKey = crypto.randomUUID();
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, entitySecretCiphertext: freshCipher, name: "arc-treasury" })
}).then(r => r.json());
const walletSetId = setRes.data?.walletSet?.id;
const walletCipher = crypto.publicEncrypt(
  { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
  Buffer.from(entitySecret, "hex")
).toString("base64");
const walletRes = await fetch(`${API}/developer/wallets`, {
  method: "POST", headers: H,
  body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: walletCipher,
                          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("VITE_CIRCLE_TREASURY_ADDRESS =", wallet.address);
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 paid from the treasury wallet, no MetaMask, no Hardhat):
- Install: `bun add solc`
- 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).
// Run once: `node scripts/register-entity-secret.mjs` (below) to mint CIRCLE_ENTITY_SECRET.
// Then: `node scripts/deploy-arc.mjs <ContractName> <ConstructorArg1> ...`
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
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());
  const pub = crypto.createPublicKey(data.publicKey);
  return crypto.publicEncrypt(
    { key: pub, oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
    Buffer.from(ES, "hex")
  ).toString("base64");                                       // single-use ciphertext, 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);
const ctor = abi.find(x => x.type === "constructor");
const sig  = ctor ? `constructor(${ctor.inputs.map(i => i.type).join(",")})` : "constructor()";

const body = { walletId: WID, blockchain: "ARC-TESTNET", name, bytecode,
  abiFunctionSignature: sig, abiParameters: ctorArgs,
  entitySecretCiphertext: await encryptEntitySecret(),
  fee: { type: "level", config: { feeLevel: "MEDIUM" } } };
const { data: { contractId } } = await fetch(`${API}/contracts/deploy`,
  { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json());

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.state === "COMPLETE") addr = contract.contractAddress;
}
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 ProvenancePalette`
- The script writes the address + ABI to `src/data/contract.json`; the UI links to `https://testnet.arcscan.app/address/<address>`.

VERIFY (arcscan / Blockscout REST — no API key, no Etherscan, no Hardhat plugin):
```js
// scripts/verify-arc.mjs — Blockscout REST verify. NO API key. NO plugin.
import fs from "node:fs";
const addr = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")).address;
const source = fs.readFileSync(process.argv[2], "utf8");     // path to flattened .sol
const form = new FormData();
form.append("compiler_version",  "v0.8.24+commit.e11b9ed9"); // match the solc you compiled with
form.append("license_type",      "mit");
form.append("optimization",      "true");
form.append("optimization_runs", "200");
form.append("source_code",       source);
const res = await fetch(`https://testnet.arcscan.app/api/v2/smart-contracts/${addr}/verification/via/flattened-code`,
  { method: "POST", body: form });
console.log(res.status, await res.text());                   // poll GET /api/v2/smart-contracts/{addr} for is_verified
```
- Flatten with `bunx sol-merger contracts/ProvenancePalette.sol build/flat.sol` before running.
- 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.
2. TREASURY CARD (mandatory UI): render a callout that reads
   `import.meta.env.VITE_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 the env var is empty, render: "Run `node scripts/bootstrap-circle.mjs` first."
3. After the user creates a physical palette digitization, 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. 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.
- VITE_CIRCLE_TREASURY_ADDRESS     The wallet address, exposed to the UI faucet card.

--- 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.

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
$50M
art collector memorabilia
SOM
$1.2M
NFT palette collector sales

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

See also

Adjacent entries.