🎵 Music & Sound Design · sound libraries

Sound Effect NFT Vault

Mint and trade exclusive sound effects as NFTs ensuring ownership and monetization for creators.

Soroban smart contract· onchain logic
Section · Onchain

The primitive.

full primer →

Sound libraries gets a tiny Rust Soroban contract deployed to the Stellar Testnet; musicians see a 'verified onchain' badge with the live contract ID and a one-tap Stellar Expert link.

Why this primitiveSmart contracts enable unique, verifiable ownership of sound assets.

Kernel
a Rust Soroban contract compiled to wasm, deployed to Stellar Testnet with the Stellar CLI, and inspectable on Stellar Expert
Drives the UI as
a 'verified onchain' badge with the live contract ID and a Stellar Expert link
Appendix · Secrets

Required keys.

STELLAR_DEPLOYER_SECRET
S… secret key of a funded Testnet account. Fund via Friendbot.
open ↗
SOROBAN_RPC_URL
Soroban RPC endpoint. Public: https://soroban-testnet.stellar.org.
open ↗
HORIZON_URL
Horizon endpoint. https://horizon-testnet.stellar.org.
open ↗
STELLAR_NETWORK_PASSPHRASE
"Test SDF Network ; September 2015" for Testnet.
open ↗
PRIVY_APP_ID
Privy app ID (public). Provisions the embedded Stellar wallet on email / Google sign-in.
open ↗
PRIVY_APP_SECRET
Privy app secret (server-only). Verifies the user token and raw-signs the tx hash with the embedded Ed25519 key.
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 six secrets above are set first. read the build strategy →

Build "Sound Effect NFT Vault" in ONE Lovable message. Single-page demo on Stellar.

CONCEPT
Mint and trade exclusive sound effects as NFTs ensuring ownership and monetization for creators.
Discipline: Music & Sound Design (sound libraries).
Onchain primitive: Soroban smart contract. Why this primitive: Smart contracts enable unique, verifiable ownership of sound assets.

5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no server auth.
- ONE Soroban contract in Rust, <=120 lines, deployed to Stellar Testnet.
- Privy embedded wallets (email + Google sign-in) are the auth + signing layer. Privy provisions a Stellar Ed25519 wallet in-browser for each user — no extension, no QR code, no WalletConnect.
- 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
- TanStack Start on Cloudflare workerd, bun, Vite 7. Server code runs in a Worker: `process.env.*` is undefined at module scope — read secrets inside `createServerFn` handler bodies only.
- Data + tx flow: client sends { privyToken, ...payload } to a `createServerFn`. Handler verifies the token, derives the user's embedded Stellar wallet, builds + prepares a Soroban tx, asks Privy to raw-sign the 32-byte tx hash, submits via Soroban RPC, polls, returns { hash }. Client renders the Stellar Expert link. Never accept a client-supplied `author` address.

--- CLIENT (browser only) ---
- `bun add @privy-io/react-auth`. Mount PrivyProvider EXCLUSIVELY from a lazy client-only chunk. Do NOT import @privy-io/react-auth from any route file, from __root.tsx, or from any module that SSR reaches — its transitive @solana/web3.js pulls rpc-websockets, which lacks the workerd export condition and hard-crashes the build with `"." is not exported under the conditions ["workerd", ...] from rpc-websockets`.

  // src/routes/index.tsx
  import { lazy } from "react";
  import { ClientOnly } from "@tanstack/react-router";
  const Demo = lazy(() => import("@/components/sound_effect_nft_vault-demo"));
  export const Route = createFileRoute("/")({
    component: () => (
      <ClientOnly fallback={<div className="h-64 animate-pulse bg-card" />}>
        <Demo />
      </ClientOnly>
    ),
  });

- Vite stub for `rpc-websockets` — REQUIRED even with the lazy import, because Vite still resolves during SSR analysis. Add to vite.config.ts:

  import path from "node:path";
  import type { Plugin } from "vite";
  const SSR_STUB = path.resolve(__dirname, "src/lib/empty-ssr-stub.ts");
  function stubBrowserOnlyPackages(): Plugin {
    return {
      name: "stub-browser-only-packages",
      enforce: "pre",
      resolveId(id) {
        if (id === "rpc-websockets" || id.startsWith("rpc-websockets/")) return SSR_STUB;
        return null;
      },
    };
  }
  // pass in defineConfig({ vite: { plugins: [stubBrowserOnlyPackages()] } })

  Then `src/lib/empty-ssr-stub.ts` is literally: `export {};`
  Do NOT use ssr.external / resolve.external — the Lovable tanstack config blocks externals for the Worker environment.

- The demo component (`src/components/sound_effect_nft_vault-demo.tsx`) is the ONLY place that imports @privy-io/react-auth. Shape:

  import { PrivyProvider, usePrivy, useLogin } from "@privy-io/react-auth";
  import { useServerFn } from "@tanstack/react-start";
  import { getPrivyConfig } from "@/lib/privy-config.functions";
  // Fetch the Privy app ID at runtime (never VITE_), then mount PrivyProvider:
  <PrivyProvider appId={appId} config={{ loginMethods: ["email","google"], appearance: { theme: "dark" } }}>
    <DemoInner />
  </PrivyProvider>
  // Inside DemoInner: const { ready, authenticated, user, getAccessToken } = usePrivy();
  //                   const { login } = useLogin();
  //                   if (!ready) return <Skeleton/>; if (!authenticated) return <button onClick={()=>login()}>Sign in</button>;
  //                   const privyToken = await getAccessToken(); await logMove({ data: { privyToken, cid } });

--- SERVER ---
- `bun add @stellar/stellar-sdk @privy-io/server-auth @privy-io/node`.
- File split (critical — top-level code in *.functions.ts ships to the client bundle; only handler bodies are stripped):
    src/lib/stellar.server.ts           — sorobanServer(), horizonUrl(), networkPassphrase() using process.env
    src/lib/privy.server.ts             — verifyPrivyToken, getOrCreateStellarWalletFor, privyRawSignHash
    src/lib/sound_effect_nft_vault.functions.ts — createServerFn wrappers (dynamic-import everything server-only)
    src/lib/privy-config.functions.ts   — public createServerFn returning { appId: process.env.PRIVY_APP_ID }
    src/data/contract.json              — { address, explorer: "https://stellar.expert/explorer/testnet" }

- privy.server.ts — two separate Privy clients (both required, singletons):
    import { PrivyClient as PrivyAuthClient } from "@privy-io/server-auth";
    import { PrivyClient as PrivyNodeClient } from "@privy-io/node";
    // privyAuth().verifyAuthToken(token) -> { userId }
    // privyNode().wallets().list({ chain_type: "stellar", external_id }) / .create({...}) / .rawSign(walletId, { params: { hash: "0x"+hex } })
    // external_id = did.replace(/[^a-zA-Z0-9_-]/g,"_").slice(0,64)

- Canonical write handler (sound_effect_nft_vault.functions.ts):

  export const logMove = createServerFn({ method: "POST" })
    .inputValidator((i: { privyToken: string; cid: string }) => {
      if (typeof i?.privyToken !== "string" || i.privyToken.length < 8) throw new Error("privyToken required");
      if (typeof i?.cid !== "string" || !i.cid.length || i.cid.length > 200) throw new Error("cid 1..200");
      return i;
    })
    .handler(async ({ data }) => {
      // Dynamic imports keep server-only deps out of the client bundle.
      const { Address, Contract, TransactionBuilder, nativeToScVal, BASE_FEE, rpc: StellarRpc } = await import("@stellar/stellar-sdk");
      const { sorobanServer, networkPassphrase } = await import("./stellar.server");
      const { verifyPrivyToken, getOrCreateStellarWalletFor, privyRawSignHash } = await import("./privy.server");
      const contractCfg = (await import("@/data/contract.json")).default;

      const userDid = await verifyPrivyToken(data.privyToken);
      const wallet = await getOrCreateStellarWalletFor(userDid);        // creates on first call
      const author = wallet.address;

      const server = sorobanServer();
      const account = await server.getAccount(author);
      const contract = new Contract(contractCfg.address);
      const op = contract.call("log", new Address(author).toScVal(), nativeToScVal(data.cid, { type: "string" }));
      const built = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: networkPassphrase() })
        .addOperation(op).setTimeout(180).build();

      // prepareTransaction attaches Soroban footprint + auth. Sign the PREPARED tx hash.
      const prepared = await server.prepareTransaction(built);
      const hashHex = Buffer.from(prepared.hash()).toString("hex");
      const sigBytes = await privyRawSignHash(wallet.id, hashHex);      // 64-byte Ed25519 signature
      prepared.addSignature(author, Buffer.from(sigBytes).toString("base64"));

      const sent = await server.sendTransaction(prepared);
      if (sent.status === "ERROR") throw new Error("send ERROR: " + JSON.stringify(sent.errorResult?.toXDR("base64") ?? sent));
      const start = Date.now();
      let last;
      while (Date.now() - start < 45_000) {
        last = await server.getTransaction(sent.hash);
        if (last.status !== StellarRpc.Api.GetTransactionStatus.NOT_FOUND) break;
        await new Promise(r => setTimeout(r, 1500));
      }
      if (last?.status !== StellarRpc.Api.GetTransactionStatus.SUCCESS) throw new Error("tx " + sent.hash + " ended " + last?.status);
      return { hash: sent.hash };
    });

- Read-only Horizon (no key needed) for the "is my wallet funded?" check:
    const res = await fetch(`${horizonUrl()}/accounts/${address}`);
    if (res.status === 404) return { funded: false, xlm: null };       // send user to https://stellarfaucet.lovable.app
    const b = await res.json(); const native = b.balances?.find(x => x.asset_type === "native");
    return { funded: true, xlm: native?.balance ?? "0" };

- Client middleware: TanStack Start's `functionMiddleware` already forwards headers; you do NOT need Supabase auth middleware here — Privy auth is inside the handler.

--- CONTRACT DEPLOY (sandbox, one-shot) ---
    cd contracts-soroban/sound_effect_nft_vault && stellar contract build
    stellar contract deploy \
      --wasm target/wasm32v1-none/release/sound_effect_nft_vault.wasm \
      --source "$STELLAR_DEPLOYER_SECRET" \
      --rpc-url "$SOROBAN_RPC_URL" \
      --network-passphrase "Test SDF Network ; September 2015"
  Write the returned C... contract id to `src/data/contract.json`. UI links to
  `https://stellar.expert/explorer/testnet/contract/<address>` and `/tx/<hash>`.
  Note: recent stellar-cli builds emit to `target/wasm32v1-none/`, older ones to `target/wasm32-unknown-unknown/`. Check whichever exists.

FAILURE MODES (know these — they burn a lot of credits):
- Build error `"." is not exported under the conditions ["workerd", ...] from rpc-websockets` -> a route or SSR-reachable module statically imports @privy-io/react-auth. Move it into the lazy client chunk AND confirm the vite stub is registered.
- Runtime `SOROBAN_RPC_URL is not set` -> you read process.env at module scope. Move the read into the handler.
- Runtime `Privy raw_sign returned no signature` -> Stellar is not enabled in the Privy dashboard's Embedded Wallets settings.
- `sendTransaction ERROR: op_bad_auth` -> you signed toXDR/pre-simulation bytes/pre-hashed on client. Sign exactly `prepared.hash()` and base64-encode the returned signature.
- `op_no_source_account` or Horizon 404 for the user's address -> account is unfunded. Direct them to https://stellarfaucet.lovable.app.
- Privy popup blank inside the Lovable preview iframe -> preview URL is not in Privy's Allowed Domains.
- Type errors on SorobanRpc.Server -> the SDK renamed it. Use `import { rpc as StellarRpc } from "@stellar/stellar-sdk"` and `StellarRpc.Server` / `StellarRpc.Api.GetTransactionStatus`.
- Contract call reverts silently -> you skipped `server.prepareTransaction(built)`. That call attaches the Soroban auth footprint the contract's `require_auth()` needs.

VERIFICATION CHECKLIST before shipping:
- [ ] `bun run build:dev` succeeds — no rpc-websockets resolver error.
- [ ] Signing in with a fresh Google account shows a G... address distinct from any pre-existing account.
- [ ] After funding via https://stellarfaucet.lovable.app, the primary action produces a Stellar Expert tx link where the source account matches the user's embedded wallet.
- [ ] `src/data/contract.json` is committed with the live C... address.
- [ ] "Sound Effect NFT Vault" footer credit line renders (see CREDIT section).
FORBIDDEN (these will break the build or leak funds):
- No @creit.tech/stellar-wallets-kit, no Lobstr, no WalletConnect / Reown, no QR-based signing.
- No ethers / viem / wagmi / RainbowKit / MetaMask / any EVM wallet library.
- No Hardhat / Foundry / Solidity / OpenZeppelin ERC-721. Contracts are Rust + soroban-sdk.
- No Alchemy Ethereum RPC / Infura / Etherscan / Sepolia / chainId 11155111.
- No VITE_PRIVY_APP_ID, VITE_SOROBAN_RPC_URL, VITE_HORIZON_URL, or VITE_PRIVY_APP_SECRET. RPC + Horizon URLs stay server-only; the Privy app ID is fetched at runtime via a public server function.
- No static import of @privy-io/react-auth from a route file, from __root.tsx, or from any module reachable during SSR. Mount PrivyProvider only from a client-only lazy chunk (see STACK).
- No top-level `process.env.SOROBAN_RPC_URL` / `process.env.PRIVY_APP_SECRET` reads. Workerd injects env per request; module-scope reads return undefined. Read inside the handler.
- Never sign `prepared.toXDR()`, never pre-hash on the client, never sign the pre-simulation build. Always sign the 32-byte `prepared.hash()`.
- Never accept a client-supplied `author` address in a server function. Always derive it from `verifyPrivyToken(privyToken)`; otherwise a caller can spend a foreign funded account.

CONTRACT (contracts-soroban/sound_effect_nft_vault/src/lib.rs):
```rust
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Symbol, symbol_short};

// SoundEffectNFTVault — Mint and trade exclusive sound effects as NFTs ensuring ownership and monetization for creators.
// Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14

const LOGS: Symbol = symbol_short!("LOGS");

#[contracttype]
#[derive(Clone)]
pub struct Entry {
    pub author: Address,
    pub cid: String,
    pub at: u64,
}

#[contract]
pub struct SoundEffectNFTVault;

#[contractimpl]
impl SoundEffectNFTVault {
    /// Log an IPFS CID (or short payload) on-chain. Author must authorize.
    pub fn log(env: Env, author: Address, cid: String) -> u32 {
        author.require_auth();
        let mut list: soroban_sdk::Vec<Entry> = env.storage().persistent().get(&LOGS).unwrap_or(soroban_sdk::Vec::new(&env));
        list.push_back(Entry { author, cid, at: env.ledger().timestamp() });
        let id = list.len();
        env.storage().persistent().set(&LOGS, &list);
        id
    }

    pub fn count(env: Env) -> u32 {
        let list: soroban_sdk::Vec<Entry> = env.storage().persistent().get(&LOGS).unwrap_or(soroban_sdk::Vec::new(&env));
        list.len()
    }
}
```

USER FLOW
1. Land on page -> "Sign in with Google or email (Privy)" -> Privy provisions a Stellar embedded wallet -> browser shows the G... address with a Copy button and live XLM balance. Unfunded accounts show a "Fund" link to https://stellarfaucet.lovable.app.
2. User performs a sound libraries action; the app invokes `log(author, payload)` on the contract through the user's Privy embedded wallet (server signs the tx hash via Privy raw_sign) and shows the Stellar Expert link as proof.
3. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets). All server-only. Do NOT add VITE_ copies.
- PRIVY_APP_ID              Privy app ID (public, but fetched at runtime by the client via a public server fn — never hardcoded, never VITE_). Dashboard: https://dashboard.privy.io
- PRIVY_APP_SECRET          Privy app secret (server-only). Used to verify user access tokens and to raw-sign the tx hash with the embedded Ed25519 key.
- SOROBAN_RPC_URL           https://soroban-testnet.stellar.org (public) or a private endpoint.
- HORIZON_URL               https://horizon-testnet.stellar.org
- STELLAR_NETWORK_PASSPHRASE  "Test SDF Network ; September 2015"
- STELLAR_DEPLOYER_SECRET   S... secret key of a funded Testnet account (used ONLY for the one-shot contract deploy from the sandbox — never read at runtime). Fund it: https://stellarfaucet.lovable.app
- PINATA_JWT                IPFS uploads (only if app pins media). Docs: https://docs.pinata.cloud/llms-full.txt

Privy dashboard one-time setup (all three are required before the first sign-in works):
1. Login methods -> enable Email AND Google.
2. Embedded wallets -> enable Stellar (OFF by default; without it wallets.create({ chain_type: "stellar" }) returns chain_not_supported).
3. Allowed domains -> add BOTH the Lovable preview URL (id-preview--<uuid>.lovable.app) AND the published URL. Missing the preview URL silently breaks sign-in inside the editor iframe with a CORS failure.

CREDIT (must appear in UI footer AND as a comment header 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
$800M
sound effect market for media
SAM
$200M
digital sound asset sales
SOM
$25M
niche sound NFT collectors

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

See also

Adjacent entries.