Quest Chain Archive
Distribute permanent quest data and decision trees for replayable and modifiable game adventures.
USDC / EURC / cirBTC payments· stablecoin flows
Section · Onchain
full primer →The primitive.
Every interactive quest design settles in the user's chosen stablecoin — USDC, EURC or cirBTC — on Arc Testnet; game designers pick a token, confirm once, and get an arcscan tx link with sub-second finality.
Why this primitiveIPFS via Pinata preserves quest data immutably, enabling decentralized quest sharing.
Kernel
ERC-20 payment flows across USDC, EURC and cirBTC on Arc Testnet — priced in the user's chosen stablecoin, settled onchain with sub-second finality
Drives the UI as
a 'pay with USDC · EURC · cirBTC' token switcher that returns an arcscan tx link the instant the transfer confirms
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 ↗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 "Quest Chain Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet.
CONCEPT
Distribute permanent quest data and decision trees for replayable and modifiable game adventures.
Discipline: Game Design & Interactive Media (interactive quest design).
Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata preserves quest data immutably, enabling decentralized quest sharing.
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/QuestChainArchive.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
/// @title PaymentQuestChainArchive
/// @notice Distribute permanent quest data and decision trees for replayable and modifiable game adventures.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract PaymentQuestChainArchive {
// token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain)
event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at);
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
function log(address token, uint256 amount, string calldata cid) external {
emit Logged(msg.sender, token, amount, cid, block.timestamp);
}
}
```
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 QuestChainArchive`
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/QuestChainArchive.sol QuestChainArchive`
- 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. User picks USDC / EURC / cirBTC to pay for a interactive quest design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link.
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
Market sizing.
TAM
$800M
global interactive quest tools
SAM
$120M
indie game narrative tools
SOM
$8M
experimental quest designers
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
game narrative archiving
Pixel Lore Vault
Permanently store and share evolving game storylines and lore across player communities.
character customization dataAvatar Trait Forge
Save and share unique character traits and skins securely and permanently on IPFS.
extended reality content storageXR Scene Snapshot
Pin immersive XR scenes and metadata permanently for cross-platform sharing and reuse.
map generation and sharingProcedural Map Depository
Securely archive and share procedurally generated game maps for community use and collaboration.