🎮 Game Design & Interactive Media · XR gesture interaction

Gesture-Control Assets

Mint unique XR gesture control assets as NFTs ensuring original creator rights.

NFT provenance mint· onchain authorship
Section · Onchain

The primitive.

full primer →

Game designers mint each XR gesture interaction as an ERC-721 token on Plasma Testnet pointing at an IPFS CID, so authorship and timestamp are provable from a single Plasmascan link.

Why this primitiveNFT provenance binds gesture assets securely to creators’ IPFS content.

Kernel
an ERC-721 contract on Plasma Testnet that mints a creator-owned token pointing at an IPFS CID, verified on Plasmascan
Drives the UI as
a 'mint to claim authorship' button that returns the tokenId, owner address, and Plasmascan link
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on Plasma Testnet via openfaucet.org (free XPL).
open ↗
PLASMA_RPC_URL
Plasma Testnet HTTPS RPC. Defaults to https://testnet-rpc.plasma.to; use a partner RPC for hackathon load.
open ↗
PRIVY_APP_ID
Enables Google sign-in and the embedded wallet on Plasma.
open ↗
PINATA_JWT
Pins images / JSON / manifests to IPFS.
open ↗

Add these in your Lovable project under Settings → Secrets before pasting the prompt below. Plasmascan verification needs no API key.

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 "Gesture-Control Assets" in ONE Lovable message. Single-page demo.

CONCEPT
Mint unique XR gesture control assets as NFTs ensuring original creator rights.
Discipline: Game Design & Interactive Media (XR gesture interaction).
Onchain primitive: NFT provenance mint. Why this primitive: NFT provenance binds gesture assets securely to creators’ IPFS content.

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 Plasma Testnet, verified on Plasmascan.
- Privy is the auth layer (Google login, embedded wallet). Users pay their own gas in XPL.
- 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.

STACK
- React + Vite single page (the index route).
- Target chain is **Plasma Testnet** (chainId 9746, native token XPL, ~1s blocks,
  full EVM). RPC: https://testnet-rpc.plasma.to. Explorer: https://testnet.plasmascan.to.
  Faucet: https://openfaucet.org/.
- Define the chain inline for viem (Privy accepts any viem chain, no built-in export needed):
    const plasmaTestnet = {
      id: 9746, name: 'Plasma Testnet',
      nativeCurrency: { name: 'XPL', symbol: 'XPL', decimals: 18 },
      rpcUrls: { default: { http: [import.meta.env.VITE_PLASMA_RPC_URL || 'https://testnet-rpc.plasma.to'] } },
      blockExplorers: { default: { name: 'Plasmascan', url: 'https://testnet.plasmascan.to' } },
    } as const;
- 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.
- PrivyProvider config — pass the plasmaTestnet chain via supportedChains and
  defaultChain so the embedded wallet lands on the right network:
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google','email'],
                supportedChains:[plasmaTestnet], defaultChain: plasmaTestnet,
                embeddedWallets:{ ethereum:{ createOnLogin:'users-without-wallets' } },
                appearance:{ theme:'dark' } }}>
- Read the embedded wallet from useWallets, not user.wallet:
    const embedded = wallets.find(w => w.walletClientType === 'privy');
- Every send goes through Privy `useSendTransaction` with `address` and
  `chainId: 9746`. Do NOT pass `sponsor: true` — Privy's native gas sponsorship
  does not cover Plasma; users pay their own XPL. Wrap the call in a 45s
  Promise.race timeout that tells the user to top up XPL if their balance is 0:
    await Promise.race([
      sendTransaction(
        { to, data, chainId: 9746 },
        { address: embedded.address }
      ),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Plasma tx timed out. Check the embedded wallet has XPL from https://openfaucet.org/"
      )), 45_000)),
    ]);
- Preflight: before calling sendTransaction, read the wallet's XPL balance via
  a viem publicClient({ chain: plasmaTestnet, transport: http() }). If it's 0,
  render a toast + inline banner with a copy-address button and a link to
  https://openfaucet.org/ so the user can drip XPL to themselves.
- Do NOT pass uiOptions:{ showWalletUIs:false } — it aborts with
  "signal is aborted without reason". The approval sheet is fine.
- Do NOT add ZeroDev / SmartWalletsProvider / any paymaster URL. Plasma
  Testnet uses simple EOA transactions.
- src/lib/pinata.ts uploads via `fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', { method:'POST', headers:{ Authorization: `Bearer ${import.meta.env.VITE_PINATA_JWT}` }, body: fd })`.
- Hardhat lives in /contracts (kept outside the Vite bundle). Install
  `@nomicfoundation/hardhat-toolbox` and `@nomicfoundation/hardhat-verify@latest`.
- hardhat.config.cjs — Plasma Testnet network + Plasmascan (Etherscan-compatible) verify:
    require("@nomicfoundation/hardhat-toolbox");
    require("@nomicfoundation/hardhat-verify");
    module.exports = {
      solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
      networks: { plasmaTestnet: {
        url: process.env.PLASMA_RPC_URL || "https://testnet-rpc.plasma.to",
        accounts: [process.env.METAMASK_PRIVATE_KEY.startsWith("0x")
          ? process.env.METAMASK_PRIVATE_KEY : "0x" + process.env.METAMASK_PRIVATE_KEY],
        chainId: 9746,
      } },
      etherscan: {
        apiKey: { plasmaTestnet: process.env.PLASMASCAN_API_KEY || "empty" },
        customChains: [{
          network: "plasmaTestnet",
          chainId: 9746,
          urls: { apiURL: "https://testnet.plasmascan.to/api",
                  browserURL: "https://testnet.plasmascan.to/" },
        }],
      },
      sourcify: { enabled: false },
    };
- Deploy: `npx hardhat run scripts/deploy.cjs --network plasmaTestnet`.
- Verify (right after deploy, no constructor args for these contracts):
  `npx hardhat verify --network plasmaTestnet <address>`
  Source becomes readable at `https://testnet.plasmascan.to/address/<address>#code`.
- Frontend reads via viem: `createPublicClient({ chain: plasmaTestnet,
  transport: http(import.meta.env.VITE_PLASMA_RPC_URL) })`. Expose the RPC to
  the client by also setting VITE_PLASMA_RPC_URL to the same value.
- Write the deployed address to `src/data/contract.json` so the UI links to
  `https://testnet.plasmascan.to/address/<address>`.

CONTRACT (contracts/GestureControlAssets.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
/// @title GestureControlAssets
/// @notice ERC-721 provenance for: Mint unique XR gesture control assets as NFTs ensuring original creator rights.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract GestureControlAssets is ERC721 {
    uint256 public nextId;
    mapping(uint256 => string) public cidOf;
    constructor() ERC721("GestureControlAssets", "GESTUR") {}
    /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
    function mint(string calldata cid) external returns (uint256 id) {
        id = ++nextId; cidOf[id] = cid; _safeMint(msg.sender, id);
    }
    function tokenURI(uint256 id) public view override returns (string memory) {
        return string(abi.encodePacked("ipfs://", cidOf[id]));
    }
}
```

USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned on Plasma Testnet.
2. If wallet XPL balance is 0, show the openfaucet.org top-up banner with copy-address.
3. After the user creates a XR gesture interaction artefact, pin the file to IPFS via Pinata, then call `mint(cid)` on the deployed contract through Privy's embedded wallet. Show tokenId, IPFS preview (`https://gateway.pinata.cloud/ipfs/<cid>`), and a Plasmascan mint-tx link.
4. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- METAMASK_PRIVATE_KEY  Plasma Testnet deployer key. Fund it (free XPL): https://openfaucet.org/ or https://faucet.quicknode.com/plasma
- PLASMA_RPC_URL        Plasma Testnet HTTPS endpoint. Default: https://testnet-rpc.plasma.to (public). For hackathon load, use a partner RPC (see https://docs.plasma.org/docs/plasma-chain/tools/rpc-providers).
- PRIVY_APP_ID          Google sign-in + embedded wallet. 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
Optional:
- PLASMASCAN_API_KEY    Not required. Plasmascan's Etherscan-compatible API accepts any string; pass "empty".

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
$900M
XR interaction assets
SAM
$150M
indie gesture creators
SOM
$15M
blockchain gesture sales

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

See also

Adjacent entries.