🎮 Game Design & Interactive Media · virtual identity

Sponsored XR Avatars

Equip XR avatars with sponsored token transactions and easy wallet sign-in.

Privy social + sponsored tx· wallet UX
Section · Onchain

The primitive.

full primer →

Game designers sign in with Google through Privy — no seed phrase, no MetaMask popup — and their virtual identity actions are sent as faucet-funded ETH transactions so they never see gas.

Why this primitivePrivy embedded wallet and sponsored tx streamline avatar customization gaslessly.

Kernel
Privy embedded wallet bootstrapped with PRIVY_APP_ID — Google sign-in plus faucet-funded transactions so users never see gas
Drives the UI as
a one-click 'Sign in with Google' that drops the user straight into the app with an onchain identity
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on BOB Mainnet via the Google Cloud faucet.
open ↗
ROOTSTOCK_TESTNET_RPC_URL
Alchemy BOB Mainnet HTTPS endpoint. Create a free app → copy the HTTPS URL.
open ↗
ROOTSTOCK_EXPLORER_API_KEY
Required for npx hardhat verify after deploy.
open ↗
PRIVY_APP_ID
Enables Google sign-in and faucet-funded transactions.
open ↗
PINATA_JWT
Pins images / JSON / manifests to IPFS.
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 "Sponsored XR Avatars" in ONE Lovable message. Single-page demo. This prompt is self-contained: everything you need is below — do not ask follow-up questions.

CONCEPT
Equip XR avatars with sponsored token transactions and easy wallet sign-in.
Discipline: Game Design & Interactive Media (virtual identity).
Onchain primitive: Privy social + sponsored tx. Why this primitive: Privy embedded wallet and sponsored tx streamline avatar customization gaslessly.

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, <=80 lines, deployed to BOB Sepolia, verified on BOB Sepolia Explorer (Blockscout).
- Privy is always the auth + sponsored-tx layer (Google login, embedded wallet).
- Pinata/IPFS only if the idea genuinely needs to store a file or metadata.
- At most ONE AI call per user action (use Lovable AI Gateway with LOVABLE_API_KEY if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

NETWORK — BOB Sepolia (testnet, everything below is required verbatim)
- chainId 808813 (hex 0xc57cd). BOB is an OP-Stack L2 secured by Bitcoin.
- Gas token is ETH. Never BTC, and never the BOB ERC-20 token — holding BOB
  tokens on Ethereum L1 does not pay for gas on the BOB network.
- RPC:      https://bob-sepolia.rpc.gobob.xyz
- Explorer: https://bob-sepolia.explorer.gobob.xyz  (Blockscout, not Etherscan)
- Funding the deployer: claim Sepolia ETH at https://sepolia-faucet.pk910.de/
  then bridge it to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- Reference: https://docs.gobob.xyz/docs/user-hub/networks
- Create src/lib/chains.ts exactly like this:
```ts
import type { Chain } from "viem";

export const bobSepolia: Chain = {
  id: 808813,
  name: "BOB Sepolia",
  nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: {
    default: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
    public: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
  },
  blockExplorers: {
    default: { name: "BOB Sepolia Explorer", url: "https://bob-sepolia.explorer.gobob.xyz" },
  },
  testnet: true,
};

export const EXPLORER = "https://bob-sepolia.explorer.gobob.xyz";
```

FILE LAYOUT (create every one of these — nothing else)
```text
contracts/SocialLogSponsoredXRAvatars.sol          the contract source below
hardhat.config.cjs              Blockscout verify config below
scripts/deploy.cjs              deploy script below
src/lib/chains.ts               bobSepolia viem Chain (above)
src/lib/contract.ts             ABI + address helper, viem public client
src/data/contract.json          { "address": "0x…", "deployTx": "0x…" } written after deploy
src/components/privy-root.tsx   ClientOnly + Suspense + lazy wrapper
src/components/privy-client-entry.tsx  the ONLY file importing @privy-io/react-auth
src/pages/Index.tsx             the entire demo UI (single page)
```

STACK
- React + Vite single page (the index route).
- SSR-safe Privy mount is mandatory. Never import @privy-io/react-auth at
  module scope of a route file — it crashes SSR. Use
  lazy(() => import('./privy-client-entry')) inside <ClientOnly> + <Suspense>,
  and put <PrivyProvider> only inside privy-client-entry.tsx.
```tsx
// src/components/privy-root.tsx
import { lazy, Suspense, type ReactNode } from "react";
import { ClientOnly } from "@tanstack/react-router"; // or a { mounted } useEffect guard on plain Vite

const PrivyClientEntry = lazy(() => import("./privy-client-entry"));

export function PrivyRoot({ children }: { children: ReactNode }) {
  return (
    <ClientOnly fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
      <Suspense fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
        <PrivyClientEntry>{children}</PrivyClientEntry>
      </Suspense>
    </ClientOnly>
  );
}
```
- PrivyProvider config — pass bobSepolia as `defaultChain` + `supportedChains`.
  chainId is also passed per-call:
```tsx
// src/components/privy-client-entry.tsx
import { PrivyProvider } from "@privy-io/react-auth";
import { bobSepolia } from "@/lib/chains";

export default function PrivyClientEntry({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider
      appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{
        loginMethods: ["google", "email"],
        embeddedWallets: { ethereum: { createOnLogin: "users-without-wallets" } },
        defaultChain: bobSepolia,
        supportedChains: [bobSepolia],
        appearance: { theme: "dark" },
      }}
    >
      {children}
    </PrivyProvider>
  );
}
```
- Read the embedded wallet from useWallets, not user.wallet:
    const embedded = wallets.find(w => w.walletClientType === 'privy');
- Every send goes through Privy `useSendTransaction` with BOTH `address`
  and `sponsor`, wrapped in a 45s Promise.race timeout whose reject message
  names the exact dashboard toggles:
    await Promise.race([
      sendTransaction(
        { to, data, chainId: 808813 },
        { address: embedded.address, sponsor: true }
      ),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB Sepolia -> Allow transactions from the client."
      )), 45_000)),
    ]);
- Do NOT pass uiOptions:{ showWalletUIs:false } — it aborts with
  "signal is aborted without reason". The approval sheet still shows on
  the embedded-EOA path; the fee reads US$0.00.
- Do NOT add ZeroDev / SmartWalletsProvider / a paymaster URL. Native
  Privy sponsorship on BOB Sepolia works with the toggles above and nothing else.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
  -> App pays -> add custom chain "BOB Sepolia" (chainId 808813, rpc
  https://bob-sepolia.rpc.gobob.xyz) -> toggle "Allow transactions from
  the client" ON. Without this, sendTransaction hangs silently.

CONTRACT (contracts/SocialLogSponsoredXRAvatars.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SocialLogSponsoredXRAvatars
/// @notice Equip XR avatars with sponsored token transactions and easy wallet sign-in.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract SocialLogSponsoredXRAvatars {
    event Logged(address indexed author, string cid, uint256 at);
    /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
    function log(string calldata cid) external {
        emit Logged(msg.sender, cid, block.timestamp);
    }
}
```

DEPLOY + VERIFY
- Hardhat lives at the repo root with sources in /contracts (kept outside the
  Vite bundle). Install `@nomicfoundation/hardhat-toolbox` AND
  `@nomicfoundation/hardhat-verify@latest`.
- hardhat.config.cjs uses hardhat-verify's Blockscout customChain shape
  (BOB Sepolia Explorer is Blockscout-based, not Etherscan):
```js
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-verify");

module.exports = {
  solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
  networks: {
    bobSepolia: {
      url: process.env.BOB_SEPOLIA_RPC_URL || "https://bob-sepolia.rpc.gobob.xyz",
      accounts: [process.env.METAMASK_PRIVATE_KEY.startsWith("0x")
        ? process.env.METAMASK_PRIVATE_KEY : "0x" + process.env.METAMASK_PRIVATE_KEY],
      chainId: 808813,
    },
  },
  etherscan: {
    apiKey: { bobSepolia: "empty" }, // Blockscout ignores the key but the field is required
    customChains: [{
      network: "bobSepolia",
      chainId: 808813,
      urls: {
        apiURL: "https://bob-sepolia.explorer.gobob.xyz/api",
        browserURL: "https://bob-sepolia.explorer.gobob.xyz/",
      },
    }],
  },
  sourcify: { enabled: false },
};
```
```js
// scripts/deploy.cjs
const hre = require("hardhat");
const fs = require("fs");

async function main() {
  const f = await hre.ethers.getContractFactory("SocialLogSponsoredXRAvatars");
  const c = await f.deploy();
  await c.waitForDeployment();
  const address = await c.getAddress();
  console.log("deployed:", address);
  fs.writeFileSync("src/data/contract.json", JSON.stringify({
    address,
    deployTx: c.deploymentTransaction().hash,
    chainId: 808813,
    explorer: "https://bob-sepolia.explorer.gobob.xyz",
  }, null, 2));
}
main().catch((e) => { console.error(e); process.exit(1); });
```
- Deploy: `npx hardhat run scripts/deploy.cjs --network bobSepolia`.
- Verify (run RIGHT AFTER deploy, no constructor args for these contracts):
  `npx hardhat verify --network bobSepolia <address>`.
  On success Blockscout returns "Successfully verified contract" and the
  source becomes readable at
  `https://bob-sepolia.explorer.gobob.xyz/address/<address>#code`.
  Blockscout does NOT auto-verify — always run the command explicitly.
- Frontend reads: create a viem public client with the BOB Sepolia RPC —
  `createPublicClient({ chain: bobSepolia, transport: http(import.meta.env.VITE_BOB_SEPOLIA_RPC_URL ?? "https://bob-sepolia.rpc.gobob.xyz") })`.

FRONTEND CODE (src/pages/Index.tsx — adapt copy, keep the choreography)
```tsx
import { useState } from "react";
import { usePrivy, useWallets, useSendTransaction } from "@privy-io/react-auth";
import { createPublicClient, encodeFunctionData, http } from "viem";
import { bobSepolia, EXPLORER } from "@/lib/chains";
import { ABI } from "@/lib/contract";
import contract from "@/data/contract.json";

const client = createPublicClient({ chain: bobSepolia, transport: http() });

export default function Index() {
  const { ready, authenticated, login, logout } = usePrivy();
  const { wallets } = useWallets();
  const { sendTransaction } = useSendTransaction();
  const embedded = wallets.find((w) => w.walletClientType === "privy");

  const [input, setInput] = useState("");
  const [status, setStatus] = useState<"idle" | "pending" | "done" | "error">("idle");
  const [txHash, setTxHash] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  async function submit() {
    if (!embedded) return;
    setStatus("pending"); setError(null); setTxHash(null);
    try {
      const data = encodeFunctionData({ abi: ABI, functionName: "log", args: [input] });
      const res: any = await Promise.race([
        sendTransaction(
          { to: contract.address as `0x${string}`, data, chainId: 808813 },
          { address: embedded.address, sponsor: true }
        ),
        new Promise((_, r) => setTimeout(() => r(new Error(
          "Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB Sepolia -> Allow transactions from the client."
        )), 45_000)),
      ]);
      const hash = res?.hash ?? res;
      setTxHash(hash);
      await client.waitForTransactionReceipt({ hash });
      setStatus("done");
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
      setStatus("error");
    }
  }

  if (!ready) return <main className="p-10">Loading…</main>;

  return (
    <main className="min-h-screen mx-auto max-w-2xl px-6 py-16">
      <h1 className="text-4xl font-semibold tracking-tight">Sponsored XR Avatars</h1>
      <p className="mt-3 opacity-70">Equip XR avatars with sponsored token transactions and easy wallet sign-in.</p>

      {!authenticated ? (
        <button onClick={login} className="mt-8 px-6 py-3 rounded-md bg-primary text-primary-foreground">
          Sign in with Google
        </button>
      ) : (
        <section className="mt-8 space-y-4">
          <div className="text-xs font-mono opacity-60">{embedded?.address}</div>
          <input value={input} onChange={(e) => setInput(e.target.value)}
                 placeholder="Describe the virtual identity entry…"
                 className="w-full rounded-md border px-4 py-3 bg-transparent" />
          <button onClick={submit} disabled={status === "pending" || !input}
                  className="px-6 py-3 rounded-md bg-primary text-primary-foreground disabled:opacity-40">
            {status === "pending" ? "Writing to BOB Sepolia…" : "Record onchain"}
          </button>
          {txHash && (
            <a href={`${EXPLORER}/tx/${txHash}`} target="_blank" rel="noreferrer"
               className="block text-sm underline break-all">
              {status === "done" ? "Confirmed" : "Pending"} · {txHash}
            </a>
          )}
          {error && <p className="text-sm text-red-500 break-all">{error}</p>}
          <button onClick={logout} className="text-xs uppercase tracking-widest opacity-60">Sign out</button>
        </section>
      )}

      <footer className="mt-20 text-xs opacity-60">Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14</footer>
    </main>
  );
}
```
Also export the ABI from src/lib/contract.ts so the page and any read call share it.

HOOK INTEGRATION — Privy social login + sponsored tx
- Login methods are Google and email only. The embedded wallet is created on
  login for users without one, so there is no seed phrase, no extension, and no
  network-switching prompt.
- Show the user's Google avatar/email from `usePrivy().user` next to the short
  wallet address so the onchain identity reads as a normal profile.
- Every write is sponsored: the approval sheet shows a US$0.00 fee. Say
  "gas covered" in the UI rather than hiding the sheet.
- Keep a visible session state machine: signed out -> signing in -> ready ->
  writing -> confirmed, and never leave a button in an ambiguous state.

USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned.
2. Every virtual identity action the user performs is sent as a sponsored BOB Sepolia tx (`log(payload)`) and displayed with a BOB Sepolia Explorer link. No wallet popups.
3. While the tx is in flight the button reads 'Writing to BOB Sepolia…' and the hash appears as a pending link; on receipt it flips to 'Confirmed'.
4. The page re-reads chain state so a refresh shows the same result.
5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

DESIGN DIRECTION
Commit to one distinctive look drawn from game design & interactive media — specifically
virtual identity. No purple-on-white gradient hero, no Inter/Poppins default stack, no
generic three-card feature row. Pick a real typeface pairing, one accent colour
plus a near-monochrome base, and a layout with an obvious focal point. Define
colours as CSS variables/tokens; never hardcode text-white or bg-black. Dark by
default. The proof-of-onchain elements (address, tx hash, CID) should read as
deliberate typographic detail in a mono face, not as debug output.

TROUBLESHOOTING (these are the failures you will actually hit)
- Blank page / "document is not defined" on load -> @privy-io/react-auth was
  imported at module scope of a route. Move it behind the lazy client entry.
- sendTransaction never resolves, no error -> the Privy dashboard gas-sponsorship
  toggle for BOB Sepolia is off. Add the custom chain and enable "Allow
  transactions from the client".
- "signal is aborted without reason" -> you passed uiOptions:{ showWalletUIs:false }.
  Remove it.
- Tx reverts with EMPTY revert data -> you encoded a function the deployed
  contract does not have. Check the verified ABI on Blockscout before
  debugging the UI.
- Tx sits pending forever -> underpriced. Resend the same nonce with a higher
  maxFeePerGas (~5 gwei) and 1 gwei priority fee.
- `hardhat verify` fails with "missing API key" -> keep the dummy
  `apiKey: { bobSepolia: "empty" }` entry; Blockscout ignores the value but
  hardhat-verify requires the field.
- Explorer shows "not verified" after deploy -> Blockscout never auto-verifies.
  Run the verify command.
- Reads return nothing -> you are pointed at the wrong chain. Confirm chainId
  808813 and the BOB Sepolia RPC, not Ethereum Sepolia (11155111).

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- METAMASK_PRIVATE_KEY  BOB Sepolia deployer key. Fund flow: get Sepolia ETH from https://sepolia-faucet.pk910.de/ then bridge to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- BOB_SEPOLIA_RPC_URL   HTTPS RPC (default: https://bob-sepolia.rpc.gobob.xyz). Public RPC works for hackathon load; swap for a dedicated node provider if you need higher rate limits.
- PRIVY_APP_ID          Google sign-in + sponsored tx. Docs: https://docs.privy.io/llms-full.txt
- PINATA_JWT            IPFS uploads (only if app pins media). Docs: https://docs.pinata.cloud/llms-full.txt

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
XR avatar market
SAM
$1B
indie XR developers
SOM
$200M
gasless avatar upgrades

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

See also

Adjacent entries.