Gasless Remix Exchange
Collaborate seamlessly on remixes without blockchain fees or sign-up friction.
Lobstr wallet via WalletConnect· wallet UX
Section · Onchain
full primer →The primitive.
Musicians tap 'Connect Lobstr', scan a QR with the Lobstr app on their phone — no seed phrase in the browser — and every collaborative remixing action is signed on-device and submitted to the Stellar Testnet.
Why this primitiveSponsored transactions let users remix and share without paying gas or managing wallets.
Kernel
Stellar Wallets Kit paired with Lobstr mobile through WalletConnect v2 — a QR pairing modal, phone-side approvals, no seed phrase in the browser
Drives the UI as
a single 'Connect Lobstr' button that opens a QR modal and returns a G… Stellar address
Required keys.
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 ↗Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
Appendix · Mega-prompt
The build prompt.
budget · 1 message
Paste into a fresh Lovable project. Make sure all six secrets above are set first. read the build strategy →
Build "Gasless Remix Exchange" in ONE Lovable message. Single-page demo on Stellar.
CONCEPT
Collaborate seamlessly on remixes without blockchain fees or sign-up friction.
Discipline: Music & Sound Design (collaborative remixing).
Onchain primitive: Lobstr wallet via WalletConnect. Why this primitive: Sponsored transactions let users remix and share without paying gas or managing wallets.
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/gasless_remix_exchange-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/gasless_remix_exchange-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/gasless_remix_exchange.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 (gasless_remix_exchange.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/gasless_remix_exchange && stellar contract build
stellar contract deploy \
--wasm target/wasm32v1-none/release/gasless_remix_exchange.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.
- [ ] "Gasless Remix Exchange" 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/gasless_remix_exchange/src/lib.rs):
```rust
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Symbol, symbol_short};
// SocialLogGaslessRemixExchange — Collaborate seamlessly on remixes without blockchain fees or sign-up friction.
// 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 SocialLogGaslessRemixExchange;
#[contractimpl]
impl SocialLogGaslessRemixExchange {
/// 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. Every collaborative remixing action the user performs is submitted as a Stellar tx signed in Lobstr (`log(author, payload)`) and displayed with a Stellar Expert link.
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
Market sizing.
TAM
$800M
global remix platform software
SAM
$250M
remix collaboration tools
SOM
$12M
gasless remixing apps
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
sample library distribution
Onchain Sound Packs
Distribute sound packs with verified authenticity and instant user access via gasless wallets.
beat sellingNFT Beat Marketplace
Buy and sell beats with onchain provenance and zero gas fee transactions.
music rights managementInstant Royalty Splits
Automatically split royalties among collaborators with transparent, gasless onchain transactions.
live-set samplingGas-Free Live Sampling
Capture and share live samples onchain instantly without blockchain transaction fees.