🎵 Music & Sound Design · stem sharing & licensing

Stem Vault Provenance

Produce, upload, and license song stems with immutable authorship proof and one-click sync licensing.

Walrus decentralised storage· decentralised storage
Section · Onchain

The primitive.

full primer →

The onchain primitive runs at the right moment in the flow and surfaces a clear, verifiable result that musicians can act on without web3 jargon.

Why this primitiveWalrus blobId anchored in Move stores exact stem audio permanently, preventing unauthorized re-uploads.

Kernel
artefacts uploaded to Walrus Testnet via the publisher endpoint, returning a permanent blobId that is stored on-chain in a Move object
Drives the UI as
a 'stored on Walrus' chip with the blobId and an aggregator preview link
Appendix · Secrets

Required keys.

SUI_DEPLOYER_KEY
Sui keypair (base64) used once by `sui client publish`. Fund the address at the Sui Testnet faucet.
open ↗
SUI_WALLET_SALT
64-char random string (generate via Lovable Cloud secrets). HKDF-derives a per-user Ed25519 Sui keypair server-side.
open ↗
WALRUS_PUBLISHER_URL
https://publisher.walrus-testnet.walrus.space — used only when the idea pins media to Walrus.
open ↗
WALRUS_AGGREGATOR_URL
https://aggregator.walrus-testnet.walrus.space — reads blobs pinned above. Optional.
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 "Stem Vault Provenance" in ONE Lovable message. Single-page demo. Sui Testnet.

CONCEPT
Produce, upload, and license song stems with immutable authorship proof and one-click sync licensing.
Discipline: Music & Sound Design (stem sharing & licensing).
Onchain primitive: Walrus decentralised storage. Why: Walrus blobId anchored in Move stores exact stem audio permanently, preventing unauthorized re-uploads.

BUDGET (5 credits, hard limit)
- One page. No extra routes, no dashboards, no docs.
- Enable Lovable Cloud + Google sign-in only. No email/password, no other providers.
- One Move module (<=80 lines), published ONCE to Sui Testnet.
- All signing runs server-side in ONE createServerFn.
- Walrus only if the idea needs blob storage.
- Skip tests, skip CI, skip landing marketing.

STACK
- Whatever the Lovable session ships with (TanStack Start today; classic Vite+React works too).
- `bun add @mysten/sui zod`. No @mysten/dapp-kit, no @mysten/enoki.
- All Sui signing is server-side inside createServerFn (or the equivalent server route on the classic template).
- src/lib/walrus.ts: `PUT ${process.env.WALRUS_PUBLISHER_URL}/v1/store?epochs=5` with the file body; read via `${process.env.WALRUS_AGGREGATOR_URL}/v1/<blobId>`. Do the upload inside the same server fn (or a sibling fn) so the browser never sees keys.

AUTH + WALLET (custodial demo — be honest in the UI)
- Enable Lovable Cloud. Add Google sign-in via `supabase--configure_social_auth`
  with providers=["google"], disable_providers=["email"]. No Google Cloud
  console, no OAuth client — Lovable brokers it.
- Sign-in button:
    import { lovable } from "@/integrations/lovable";
    await lovable.auth.signInWithOAuth("google", { redirect_uri: window.location.origin });
- src/lib/sui-wallet.server.ts derives a per-user Ed25519 keypair:
    import { hkdfSync } from "node:crypto";
    import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
    export function deriveKeypair(userId: string) {
      const salt = process.env.SUI_WALLET_SALT!;
      const seed = hkdfSync("sha256", Buffer.from(salt), Buffer.from(userId),
                            Buffer.from("sui-wallet:ed25519:v1"), 32);
      return Ed25519Keypair.fromSecretKey(new Uint8Array(seed));
    }
- UI shows the derived Sui address + Suiscan link
  (https://suiscan.xyz/testnet/account/<addr>) + "Fund via Testnet faucet"
  button linking to https://faucet.sui.io/?address=<addr>.
- Disclosure copy under the address card:
  "Demo wallet held by the server — safe for Sui Testnet, do not use for real value." 

MOVE MODULE (move/sources/stem_vault_provenance.move)
```move
/// Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
module creative::stem_vault_provenance {
    use sui::event;
    use sui::tx_context::{Self, TxContext};

    public struct Logged has copy, drop {
        author: address,
        blob_id: vector<u8>,
        at: u64,
    }

    /// Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
    public entry fun log(blob_id: vector<u8>, ctx: &mut TxContext) {
        event::emit(Logged {
            author: tx_context::sender(ctx),
            blob_id,
            at: tx_context::epoch_timestamp_ms(ctx),
        });
    }
}
```

PUBLISH (one-time, from the sandbox)
- Write move/Move.toml:
    [package]
    name = "creative"
    [addresses]
    creative = "0x0"
    [dependencies]
    Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
- sui client switch --env testnet
- sui client publish --gas-budget 100000000 --json move > publish.json
- Extract packageId:
    jq '.objectChanges[] | select(.type=="published") | .packageId' publish.json
- Write src/data/sui-package.json:
    { "packageId": "0x...", "module": "<mod>", "network": "testnet", "publishedAt": "<iso>" }
  Import it in the server fn. Never hardcode the package id in a component.

SERVER FN (src/lib/stem_vault_provenance.functions.ts)
    import { createServerFn } from "@tanstack/react-start";
    import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware";
    import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";
    import { Transaction } from "@mysten/sui/transactions";
    import { z } from "zod";
    import pkg from "@/data/sui-package.json";
    import { deriveKeypair } from "./sui-wallet.server";

    const MIN_GAS_MIST = 5_000_000n;

    export const submit = createServerFn({ method: "POST" })
      .middleware([requireSupabaseAuth])
      .inputValidator((d) => z.object({ blobId: z.string().min(1).max(512) }).parse(d))
      .handler(async ({ data, context }) => {
        const kp = deriveKeypair(context.userId);
        const addr = kp.toSuiAddress();
        const client = new SuiClient({ url: getFullnodeUrl("testnet") });
        const bal = await client.getBalance({ owner: addr });
        if (BigInt(bal.totalBalance) < MIN_GAS_MIST) {
          return { ok: false, address: addr,
                   error: "Fund the wallet at https://faucet.sui.io/?address=" + addr };
        }
        const tx = new Transaction();
        tx.moveCall({
          target: `${pkg.packageId}::${pkg.module}::log`,
          arguments: [tx.pure.vector("u8", Array.from(new TextEncoder().encode(data.blobId)))],
        });
        const res = await client.signAndExecuteTransaction({ signer: kp, transaction: tx });
        return { ok: true, address: addr, digest: res.digest,
                 explorer: "https://suiscan.xyz/testnet/tx/" + res.digest };
      });

UI (single page)
- Signed out: hero + "Sign in with Google" button (the lovable.auth call above).
- Signed in: address card (copy button, faucet link, Suiscan account link) +
  disclosure copy + <action form for stem sharing & licensing> that calls the server fn via useServerFn+useMutation.
- After success: render the returned digest as a Suiscan tx link. Keep the last 5 in local state.
- Footer: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

SECRETS (Lovable Cloud -> Secrets)
- SUI_WALLET_SALT     generate 64-char random via `generate_secret`.
- SUI_DEPLOYER_KEY    Sui keypair (base64) used ONCE to publish the Move package.
                      Fund at https://faucet.sui.io/?address=<deployer-address>.
- WALRUS_PUBLISHER_URL   https://publisher.walrus-testnet.walrus.space
- WALRUS_AGGREGATOR_URL  https://aggregator.walrus-testnet.walrus.space

Suiscan note: a freshly derived address shows "no results" until its first tx confirms. That is expected, not a bug.

On submit, upload the stem sharing & licensing artefact to Walrus, then call `stem_vault_provenance::log(blob_id)` server-side. Show blobId, aggregator preview, Suiscan tx.
Appendix · Market

Market sizing.

TAM
$11B
global music software market
SAM
$2.1B
music licensing and sync platforms
SOM
$85M
indie producer stem marketplaces

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

See also

Adjacent entries.