💃 Dance & Choreography · competitive choreography

Gasless Dance Battles

Enable dancers to join and sponsor dance battles without paying gas fees.

Privy social wallet on Arc· wallet UX
Section · Onchain

The primitive.

full primer →

Choreographers sign in with Google through Privy — no seed phrase, no MetaMask popup — and their competitive choreography actions are sent on Arc Testnet with USDC gas paid transparently.

Why this primitivePrivy social with sponsored tx removes friction of blockchain fees in quick peer competitions.

Kernel
Privy embedded wallet bootstrapped with PRIVY_APP_ID — Google sign-in, embedded wallet, and Arc Testnet (chain 5042002) set as the default network so users pay gas in USDC without seeing it
Drives the UI as
a one-click 'Sign in with Google' that drops the user straight into the app with an Arc wallet already funded via Circle Faucet
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 "Gasless Dance Battles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet.

CONCEPT
Enable dancers to join and sponsor dance battles without paying gas fees.
Discipline: Dance & Choreography (competitive choreography).
Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social with sponsored tx removes friction of blockchain fees in quick peer competitions.

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/GaslessDanceBattles.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SocialLogGaslessDanceBattles
/// @notice Enable dancers to join and sponsor dance battles without paying gas fees.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract SocialLogGaslessDanceBattles {
    // 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):
- 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 GaslessDanceBattles`
- 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/GaslessDanceBattles.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. Every competitive choreography action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently.
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
$5B
global dance industry revenue
SAM
$600M
online dance battle platforms
SOM
$30M
active dance battle participants paying digitally

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

See also

Adjacent entries.