Privy Crafting Market
Trade crafted game items with gasless wallet sign-in and sponsored trades.
Lace connect· wallet primitive
Section · Onchain
full primer →The primitive.
A CIP-30 handshake with Lace. No transaction is submitted — you just prove connectivity, network, and balance.
Kernel
A CIP-30 handshake with Lace. No transaction is submitted — you just prove connectivity, network, and balance.
Drives the UI as
A single 'Connect Lace' button that resolves into an address chip, an ADA balance, and a switch-network hint if Lace is on the wrong testnet.
Required keys.
BLOCKFROST_PROJECT_ID_PREVIEW
Server-only. Get one free at blockfrost.io; create a Preview project.
open ↗Add these in Lovable Settings → Secrets before pasting the prompt below. Blockfrost keys stay server-side.
Appendix · Mega-prompt
The build prompt.
Network
SDK
Shape
PreviewFastest to iterate on. Testnet resets more often. Faucet from docs.cardano.org.
Shape · Lace connectA CIP-30 handshake with Lace. No transaction is submitted — you just prove connectivity, network, and balance.
# Privy Crafting Market
> Trade crafted game items with gasless wallet sign-in and sponsored trades.
_Context: Game Design & Interactive Media · player economies_
Build a Cardano Preview dApp on Lovable using **Lace + Mesh SDK**.
This prompt is self-contained — no external skill required.
**READ THE "Hard-won lessons" SECTION BELOW BEFORE WRITING ANY CODE.**
The order of setup steps matters; skipping ahead reproduces well-known bugs.
## Frontend stack (fixed by Lovable — do not swap)
- TanStack Start + Vite. Do NOT install Next.js, Remix, or `react-router-dom`.
- shadcn/ui + Tailwind v4 (already installed).
- Server code uses `createServerFn` from `@tanstack/react-start`. Raw HTTP
endpoints (webhook, submit proxy, Blockfrost proxy) go under
`src/routes/api/public/`.
- Runtime: Cloudflare Workers with `nodejs_compat`. Mesh, Lucid, `blakejs`,
`bech32` all work. Avoid Node-native packages (sharp, canvas, node-gyp).
## Testnet-only banner (REQUIRED, render on every page)
```tsx
<div className="bg-amber-500/10 border-b border-amber-500/30 text-amber-600 text-xs text-center py-2">
Testnet only — do not send mainnet ADA to any address on this dApp.
</div>
```
## Provider secret (Blockfrost) — server-only, with GET + POST proxy
Blockfrost project IDs are secrets. They must never touch the browser bundle.
1. In Lovable, open the secrets UI and add:
- `BLOCKFROST_PROJECT_ID_PREVIEW` = your Preview project ID from https://blockfrost.io
2. Never prefix with `VITE_`. Never read it at module scope. Read it ONLY
inside a `createServerFn(...).handler()` or a server route handler.
3. Expose TWO same-origin proxies so the browser never sees the key:
- a **read proxy** the Lucid/Mesh clients can point at for GET requests,
- a **submit proxy** for POSTing signed CBOR.
### Read proxy — splat route (GET everything under /api/v0/*)
```ts
// src/routes/api/public/cardano-blockfrost/$.ts
import { createFileRoute } from "@tanstack/react-router";
const BASE: Record<string, string> = {
preview: "https://cardano-preview.blockfrost.io/api/v0",
preprod: "https://cardano-preprod.blockfrost.io/api/v0",
};
export const Route = createFileRoute("/api/public/cardano-blockfrost/$")({
server: {
handlers: {
GET: async ({ request, params }) => {
const rest = (params as { _splat?: string })._splat ?? "";
const [network, ...tail] = rest.split("/");
const base = BASE[network];
if (!base) return new Response("Unknown network", { status: 400 });
const projectId = process.env["BLOCKFROST_PROJECT_ID_" + network.toUpperCase()];
if (!projectId) return new Response("Missing project id", { status: 500 });
const url = new URL(request.url);
const target = base + "/" + tail.join("/") + url.search;
const res = await fetch(target, { headers: { project_id: projectId } });
return new Response(await res.arrayBuffer(), {
status: res.status,
headers: { "content-type": res.headers.get("content-type") ?? "application/json" },
});
},
POST: async ({ request, params }) => {
const rest = (params as { _splat?: string })._splat ?? "";
const [network, ...tail] = rest.split("/");
const base = BASE[network];
if (!base) return new Response("Unknown network", { status: 400 });
const projectId = process.env["BLOCKFROST_PROJECT_ID_" + network.toUpperCase()];
if (!projectId) return new Response("Missing project id", { status: 500 });
const body = await request.arrayBuffer();
const res = await fetch(base + "/" + tail.join("/"), {
method: "POST",
headers: {
project_id: projectId,
"content-type": request.headers.get("content-type") ?? "application/cbor",
},
body,
});
return new Response(await res.arrayBuffer(), { status: res.status });
},
},
},
});
```
### Dedicated submit endpoint (accepts hex CBOR from the browser)
```ts
// src/routes/api/public/cardano-submit.ts
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/public/cardano-submit")({
server: {
handlers: {
POST: async ({ request }) => {
const url = new URL(request.url);
const network = url.searchParams.get("network") ?? "preview";
const projectId = process.env["BLOCKFROST_PROJECT_ID_" + network.toUpperCase()];
if (!projectId) return new Response("Missing project id", { status: 500 });
const cborHex = (await request.text()).trim();
if (!/^[0-9a-fA-F]+$/.test(cborHex)) {
return new Response("Invalid CBOR hex", { status: 400 });
}
const res = await fetch(
"https://cardano-" + network + ".blockfrost.io/api/v0/tx/submit",
{
method: "POST",
headers: { "Content-Type": "application/cbor", project_id: projectId },
body: Buffer.from(cborHex, "hex"),
},
);
return new Response(await res.text(), { status: res.status });
},
},
},
});
```
The browser talks ONLY to `/api/public/cardano-blockfrost/preview/*` and
`/api/public/cardano-submit` — never to `blockfrost.io` directly.
## Wallet: Lace (CIP-30) on Preview
1. Install Lace from https://www.lace.io/ (desktop browser extension).
2. Open Settings → Network → **Preview**.
3. Create/import a wallet and copy the receive address (`addr_test1...`).
4. Fund it at https://docs.cardano.org/cardano-testnets/tools/faucet (choose **Preview**).
### Mobile reality
Lace Mobile 1.0 has NO in-app browser for testnet dApps and NO native CIP-45
scanner. Preview/Preprod flows are **desktop-first**. Show desktop-required
messaging on mobile viewports; do not build a QR pairing flow into the MVP.
### Connect (browser-only — never call during SSR)
```ts
// src/lib/use-lace.ts
export async function connectLace() {
if (typeof window === "undefined") throw new Error("SSR");
const lace = (window as any).cardano?.lace;
if (!lace) throw new Error("Install Lace and switch to Preview.");
const api = await lace.enable();
const networkId = await api.getNetworkId(); // 0 = testnet, 1 = mainnet
if (networkId !== 0) throw new Error("Switch Lace to Preview testnet.");
return { api, address: await api.getChangeAddress() };
}
```
Gate all wallet reads with `useEffect` or `<ClientOnly>`. Never invoke
`lace.enable()` at module scope.
### Same-network invariant
`getNetworkId()` returns 0 for both Preview and Preprod. Enforce the exact
network by matching wallet + provider + UI:
```ts
const UI_NETWORK = "preview" as const;
// Refuse to submit if the Blockfrost proxy route or provider config
// disagrees with UI_NETWORK.
```
## Off-chain SDK: Mesh — AVOID IN THE BROWSER
Pick ONE SDK per project. Do NOT mix Mesh and Lucid in the same tx path.
**KNOWN BROWSER ISSUE (2026-07):** `@meshsdk/core@1.9.x` throws
`TypeError: Cannot read properties of undefined (reading 'from')` deep
inside its tx builders when bundled by Vite. The root cause is CJS/ESM
interop with `@cardano-sdk/core` + `@harmoniclabs/*` — one of the interop
shims resolves to `undefined` and the next `.from(...)` call blows up.
A Buffer polyfill does NOT fix it. Switching from the deprecated
`Transaction` class to `MeshTxBuilder` does NOT fix it. The bug is inside
`MeshTxBuilder` itself.
**What this means for you today:**
- Do NOT use `new Transaction(...)` in the browser.
- Do NOT use `new MeshTxBuilder(...)` in the browser either.
- Mesh is still safe for:
- `@meshsdk/react` wallet UI components on read-only pages
- Server-side flows that don't go through Vite's browser bundler
For any transaction you build in the browser, use **Lucid Evolution** (see
the Lucid block). Migrating Mesh → Lucid mid-project is a full rewrite of
every submit path — start on Lucid.
## App shape: Wallet-only
- Route `/` renders a Lace connect panel + address + ADA balance.
- No transactions submitted.
- Use it to confirm CIP-30 works and Lace is on the correct network.
## Tx submission checklist (every submit path must handle these)
1. **No UTxOs** — wallet empty. Surface a "Fund via faucet" CTA linking to
https://docs.cardano.org/cardano-testnets/tools/faucet.
2. **Wrong network** — provider network ≠ wallet `getNetworkId()` (or the
UI's declared network). Refuse to submit and show a "Switch Lace" hint.
3. **Min-ADA** — datum-carrying / token-carrying UTxOs need ≥ ~1.5 ADA
attached (2_000_000 lovelace is the safe default).
4. **Submit timeout** — treat submission as fire-and-forget. After the proxy
returns a hash, poll the explorer (Blockfrost `/txs/{hash}`) before
enabling the next action.
5. **User rejection** — Lace throws `{ code: 2, info: "User declined to sign" }`.
Catch and show a friendly "Signing cancelled" message; do NOT bubble the
raw error to the UI.
6. **`?debug=1` panel** — render the full `error.message + error.cause`
chain to the DOM when `?debug=1` is in the URL. See the Debug panel
block. Toasts alone lose the underlying stack.
## Known misleading errors → real cause
| Symptom in the browser | Real cause / fix |
|---|---|
| `Cannot read properties of undefined (reading 'from')` inside Mesh | `@meshsdk/core@1.9.x` CJS/ESM interop under Vite. Switch to Lucid Evolution. Buffer polyfills and swapping `Transaction` for `MeshTxBuilder` do NOT help. |
| `(void 0) is not a function` from Lucid | UPLC wasm never instantiated. Add `vite-plugin-wasm` + `optimizeDeps.exclude` (see the Vite setup block). |
| `[UNLOADABLE_DEPENDENCY] .../uplc_tx_bg.wasm` at build | Same fix — install `vite-plugin-wasm`. |
| `Buffer is not defined` inside SDK | Add the browser `Buffer` polyfill via **dynamic** `await import("buffer")` inside the submit function (see Buffer polyfill block). |
| `Duplicate __tla declaration` at build | You added `vite-plugin-top-level-await` on top of `vite-plugin-wasm`. Remove it; rely on `build.target: "es2022"`. |
| `Duplicate routes found with id: /` in dev-server log | Stale HMR artifact from a mid-edit route regeneration. Reload; do NOT delete route files. |
## Hard-won lessons — read this BEFORE writing code
These are ordered. Each one cost a real hackathon day the last time it
was ignored.
1. **Pick Lucid Evolution first, not Mesh.** Mesh v1 in the browser under
Vite throws `undefined (reading 'from')` from deep inside its tx
builders regardless of Buffer polyfills or dropping the deprecated
`Transaction` class. Migrating Mesh → Lucid mid-project rewrites every
submit path. Start on Lucid.
2. **Configure `vite-plugin-wasm` before writing any tx code.** Lucid's
UPLC dependency is wasm-pack bundler-target. Without the plugin +
`optimizeDeps.exclude` you either fail the build or ship a runtime
`(void 0) is not a function`.
3. **Use native ES2022 top-level await** (`build.target: "es2022"`).
Do NOT add `vite-plugin-top-level-await` on top of `vite-plugin-wasm` —
both inject a `__tla` symbol and the duplicate declaration fails the
build.
4. **Polyfill `Buffer` via dynamic import inside the submit function**,
not at module scope:
`const { Buffer } = await import("buffer"); (globalThis as any).Buffer ??= Buffer;`.
Module-scope `import "buffer/"` hangs the Vite optimizer.
5. **Never ship a Blockfrost project ID to the browser.** Lucid's
`Blockfrost` client wants the key at construction — proxy through
`src/routes/api/public/cardano-blockfrost/$.ts` and pass `""` as the
client's project ID.
6. **Treat "Duplicate routes found with id: /" in the dev-server log as a
stale HMR artifact.** It clears on the next successful compile. Do NOT
delete `src/routes/index.tsx` chasing it.
7. **Add the `?debug=1` panel on day 1**, not after the third mystery
`TypeError`. Cardano SDK errors are opaque; screenshots without the
`error.cause` chain waste an entire iteration.
## Red flags to avoid (auto-fail in review)
- Any `VITE_*` variable holding a Blockfrost / Maestro / Koios project ID.
- Calling `window.cardano.lace.enable()` at module scope or in an SSR loader.
- Using `new Transaction(...)` or `new MeshTxBuilder(...)` in the browser
under Vite (see Mesh block).
- Mixing Mesh and Lucid in the same tx builder.
- Missing `vite-plugin-wasm` while using Lucid.
- Adding `vite-plugin-top-level-await` on top of `vite-plugin-wasm`.
- Static `import "buffer/"` at module scope.
- Hard-coding a network in the SDK that disagrees with the wallet's reported
network — refuse to submit if they don't match.
- Any Mainnet code path (this project is testnet-only).
- Calling Blockfrost directly from the browser instead of the server proxy.
Market sizing.
TAM
$6B
in-game crafting markets
SAM
$1.2B
indie player economies
SOM
$240M
gasless item trading platforms
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
quest tracking
Onchain Quest Ledger
Securely log player quest progress and achievements on Ethereum Sepolia to prove in-game milestones.
competitive scoringImmutable Scoreboards
Store game scores on Sepolia to prevent cheating and create permanent leaderboards.
event verificationProof-of-Play Events
Authenticate player participation in timed events on Sepolia for exclusive rewards and recognition.
asset evolutionDynamic NFT Game Assets
Allow game assets to evolve on Sepolia based on gameplay, creating unique player-owned NFTs.