🎵 Music & Sound Design · NFT sound assets

Gasless Sound Design NFT

Mint and trade unique sound designs onchain with free wallet sign-ins and sponsored tx.

Durin L2 subname registrar· cheap subnames
Section · Onchain

The primitive.

full primer →

Musicians mint a subname on Base Sepolia via Durin for pennies, and the same name resolves back on L1 through CCIP-Read — every NFT sound assets entry gets a portable, cheap ENS identity.

Why this primitiveDurin puts subname minting on an L2 so Gasless Sound Design NFT can hand out one NFT sound assets name per user for pennies, while L1 CCIP-Read keeps the names resolvable from every ENS-aware surface.

Kernel
a Durin L2Registrar deployed to Base Sepolia that mints <name>.<parent>.eth as an L2 NFT, resolved on L1 via a Durin-compatible CCIP-Read resolver
Drives the UI as
a subname minter that fires the tx on Base Sepolia, then verifies the name resolves back on Ethereum Sepolia through viem
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on Sepolia via the Google Cloud faucet before registering a .eth name.
open ↗
SEPOLIA_RPC_URL
Alchemy Sepolia HTTPS endpoint. viem uses this to resolve every .eth name.
open ↗
ETHERSCAN_API_KEY
Required for npx hardhat verify on ENS resolvers / L2 registrars.
open ↗
PRIVY_APP_ID
Google sign-in + sponsored transactions so users never see gas.
open ↗
PINATA_JWT
Pin site bundles / metadata to IPFS, then write the CID as the ENS contenthash.
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 Sound Design NFT" in ONE Lovable message. Single-page demo that mints
CHEAP subnames on Base Sepolia via a Durin L2 registrar (namestonehq/durin).
This bypasses ENS DAO approval entirely — anyone can deploy a Durin L2
registrar and issue subnames on L2.

CONCEPT
Mint and trade unique sound designs onchain with free wallet sign-ins and sponsored tx.
Discipline: Music & Sound Design (NFT sound assets).
ENS primitive: Durin L2 subname registrar on Base Sepolia (84532), factory-deployed, L1-wired via CCIP-Read.
Why this primitive: Durin puts subname minting on an L2 so Gasless Sound Design NFT can hand out one NFT sound assets name per user for pennies, while L1 CCIP-Read keeps the names resolvable from every ENS-aware surface.

5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no auth
  flows beyond Privy drop-in.
- No custom Solidity unless the hook explicitly deploys one — reuse the
  canonical ENS / Durin / Namestone contracts listed below.
- Privy is always the auth + sponsored-tx layer (Google login, embedded
  wallet, native gas sponsorship on Sepolia and Base Sepolia).
- At most ONE AI call per user action (Lovable AI Gateway + LOVABLE_API_KEY
  if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

ENS SPECIFICS — Durin on Base Sepolia:

Canonical Durin factory (Base Sepolia 84532):
  L2RegistryFactory  0xDddddDdDDD8Aa1f237b4fa0669cb46892346d22d

Deploy the L2 registry ONCE (via a small Hardhat script or a one-shot admin
route protected by BASE_SEPOLIA_PRIVATE_KEY — never expose it client-side):
  factory.deploy(baseName, symbol, admin) -> L2Registry address
Then call `L2Registry.setRegistrar(deployer, true)` so your app can mint.
Save the resulting registry address to `src/data/durin.json`.

L2 mint (from the browser, sponsored by Privy on Base Sepolia):
  L2Registry.register(label, owner)
  // label = "alice" -> alice.<parent>.eth resolvable via Durin L1 resolver.

L1 wiring (OPTIONAL for the hackathon — only works if the parent `.eth`
name is registered on Sepolia). Guard the wiring step with
`ENSRegistry.owner(namehash(parent)) !== 0x0`.
  parent.resolver = Durin L1 resolver (per docs, chain-specific)
  durinL1Resolver.setL2Registry(namehash(parent), 84532, l2RegistryAddress)
If the parent isn't registered, ship the demo behind a clear banner:
  "L2-ONLY PREVIEW: subnames are minted on Base Sepolia but won't resolve
   from L1 until <parent>.eth is registered on Sepolia and pointed at the
   Durin L1 resolver."
DO NOT attempt setResolver on an unregistered parent — it reverts with no
useful reason.

Privy chain-switching for Base Sepolia (same four traps as Sepolia, but
chainId 84532 / '0x14a34'):
  await embedded.switchChain(84532);
  const p = await embedded.getEthereumProvider();
  if ((await p.request({method:'eth_chainId'})) !== '0x14a34')
    throw new Error('Provider still not on Base Sepolia — re-open Privy');

Pre-flight guards:
1. Registry address in `src/data/durin.json` is non-empty AND deployer is
   a registered registrar: `L2Registry.registrars(deployer) === true`.
2. Provider on 84532 (see above).
3. Label matches `/^[a-z0-9-]{1,63}$/` and is not already minted
   (`L2Registry.available(labelhash) === true`).

Hardhat (only for the one-time L2 deploy — keep OUTSIDE the Vite bundle,
in /contracts):
  npm i -D @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-verify@latest
  // hardhat.config.cjs — Etherscan v2 single-key shape + Basescan customChain:
  require("@nomicfoundation/hardhat-toolbox");
  require("@nomicfoundation/hardhat-verify");
  module.exports = {
    solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
    networks: {
      baseSepolia: {
        url: process.env.BASE_SEPOLIA_RPC_URL,
        accounts: [process.env.BASE_SEPOLIA_PRIVATE_KEY.startsWith("0x")
          ? process.env.BASE_SEPOLIA_PRIVATE_KEY
          : "0x" + process.env.BASE_SEPOLIA_PRIVATE_KEY],
        chainId: 84532,
      },
    },
    // Etherscan v2 uses ONE key across all chains — NOT the per-network map:
    etherscan: {
      apiKey: process.env.BASESCAN_API_KEY,
      customChains: [{ network: "baseSepolia", chainId: 84532,
        urls: { apiURL: "https://api-sepolia.basescan.org/api",
                 browserURL: "https://sepolia.basescan.org" } }],
    },
    sourcify: { enabled: false },
  };
Deploy: `npx hardhat run scripts/deployDurin.cjs --network baseSepolia`
Verify: `npx hardhat verify --network baseSepolia <registry>`
On success Basescan says "Successfully verified contract" and the source
becomes readable at https://sepolia.basescan.org/address/<addr>#code.

UNIVERSAL RESOLVER V2 (use this for EVERY name read — it is the only
reader that works across v1, ENSv2, offchain ENSIP-25 and Durin L2 names):
- Sepolia address: 0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe
- ABI subset:
    resolve(bytes name, bytes data) view returns (bytes result, address resolver)
    findCanonicalName(bytes name) view returns (bytes canonicalName, address resolver)
    reverse(bytes lookupAddress, uint256 coinType) view returns (string name, address resolver, address reverseResolver)
- `name` is DNS-WIRE-ENCODED, not a string and not a namehash:
    import { toHex, namehash } from 'viem';
    import { packetToBytes } from 'viem/ens';
    const wire = toHex(packetToBytes('alice.eth'));
- Always read through a viem PUBLIC CLIENT (CCIP-Read is on by default). Reading
  through Privy's embedded provider throws `OffchainLookup` and breaks offchain
  and L2 names.
- `ResolverNotFound` means "no resolver set", NOT "name unregistered" — show
  different copy for each.
- Reverse records are opt-in; most Sepolia addresses have none. Fall back to the
  truncated address, never "unknown".
- After every register/mint, verify with a Universal Resolver read and show the
  resolved address in the success card. It is a free call.

PRIVY (SSR-safe mount + embedded-EOA routing — the four traps):
- Never import @privy-io/react-auth at module scope of a route file. Use
    const Client = lazy(() => import('./privy-client-entry'));
  inside <ClientOnly><Suspense>...</Suspense></ClientOnly>. Put
  <PrivyProvider> only inside privy-client-entry.tsx.
- PrivyProvider config (do NOT stub defaultChain as { id, name } — pass
  viem's `sepolia` or omit; chainId is passed per-call):
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google','email'],
                embeddedWallets:{ ethereum:{ createOnLogin:'users-without-wallets' } },
                appearance:{ theme:'dark' } }}>
- TRAP 1 — smart account vs EOA. `wallets[0]` and `useSendTransaction`
  can pick an EIP-4337 smart account that is EMPTY even when your embedded
  EOA is funded. Always pick the embedded EOA explicitly:
    const embedded = wallets.find(w => w.walletClientType === 'privy');
    const provider = await embedded.getEthereumProvider();
  Display `embedded.address` (not smartAccount.address) as the fund-me box
  and poll THAT balance.
- TRAP 2 — stale RPC after switchChain. Privy caches the provider's RPC
  URL. After every switchChain, re-fetch AND assert:
    await embedded.switchChain(11155111);
    const p = await embedded.getEthereumProvider();
    if ((await p.request({method:'eth_chainId'})) !== '0xaa36a7')
      throw new Error('Provider still not on Sepolia — re-open Privy');
- TRAP 3 — `signal is aborted without reason`. Do NOT pass
  uiOptions:{ showWalletUIs:false } (or any uiOptions) on the embedded
  path. Send with ONLY { to, data, value, chainId }.
- TRAP 4 — sponsored tx hangs. Wrap every send in a 45s Promise.race
  whose reject message names the exact dashboard toggle:
    await Promise.race([
      sendTransaction({ to, data, value, chainId: 11155111 },
                     { address: embedded.address, sponsor: true }),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Privy sendTransaction timed out after 45s. In Privy dashboard: Gas sponsorship -> App pays -> add Ethereum Sepolia (and Base Sepolia if used) -> Allow transactions from the client."
      )), 45_000)),
    ]);
- Do NOT add ZeroDev / SmartWalletsProvider / paymaster URL. Native
  Privy sponsorship works with only the dashboard toggle above.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
  -> App pays -> add "Ethereum Sepolia" (and "Base Sepolia" if the hook
  uses it) -> toggle "Allow transactions from the client" ON.

STACK
- React + Vite single page. No router.
- viem public clients for BOTH sepolia and baseSepolia.
- src/data/durin.json — { registry, baseName, chainId: 84532 } after deploy.
- src/lib/durin.ts — mint + availability + L1 wiring status.

USER FLOW
1. Land -> "Sign in with Google" (Privy) -> embedded EOA on Base Sepolia.
2. Pre-flight badge shows registry deployed + provider on 84532.
3. If L1 parent unregistered, banner explains L2-only mode.
4. Enter a label -> "Claim subname on Base Sepolia" -> sponsored tx ->
   success card with Basescan link + "your L2 name" pill used throughout
   the NFT sound assets workflow.
5. Footer renders the credit line below.

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- SEPOLIA_RPC_URL (+ VITE_SEPOLIA_RPC_URL, same value) Alchemy Sepolia HTTPS endpoint. Create a free app at https://dashboard.alchemy.com/. Public RPCs throttle/fail — Alchemy required.
- BASE_SEPOLIA_RPC_URL (+ VITE_BASE_SEPOLIA_RPC_URL) Alchemy Base Sepolia HTTPS URL.
- METAMASK_PRIVATE_KEY  EOA that will deploy / own the parent name. Fund it via https://cloud.google.com/application/web3/faucet/ethereum/sepolia
- BASE_SEPOLIA_PRIVATE_KEY  EOA funded on Base Sepolia (same key OK if bridged). Faucet: https://www.alchemy.com/faucets/base-sepolia
- ETHERSCAN_API_KEY  Etherscan v2 verify. Get: https://etherscan.io/myapikey
- BASESCAN_API_KEY  Basescan v2 verify. Get: https://basescan.org/myapikey
- PRIVY_APP_ID (+ VITE_PRIVY_APP_ID) Google sign-in + sponsored tx. Docs: https://docs.privy.io/llms-full.txt

CREDIT (must appear in UI footer):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Appendix · Market

Market sizing.

TAM
$500M
NFT music asset market
SAM
$130M
sound design NFTs
SOM
$9M
gasless NFT minting

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

See also

Adjacent entries.