Technique Archive Hub
Pin and share detailed dance technique manuals and videos as permanent IPFS content.
Send test ADA· transfer primitive
Section · Onchain
full primer →The primitive.
Build, sign, and submit a plain payment transaction — the smallest possible round-trip against the Cardano ledger.
Kernel
Build, sign, and submit a plain payment transaction — the smallest possible round-trip against the Cardano ledger.
Drives the UI as
A recipient / amount form, a confirm sheet, and a receipt with the transaction hash linked out to Cardanoscan.
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 · Send test ADABuild, sign, and submit a plain payment transaction — the smallest possible round-trip against the Cardano ledger.
# Technique Archive Hub
> Pin and share detailed dance technique manuals and videos as permanent IPFS content.
_Context: Dance & Choreography · dance training materials_
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: Send test ADA
- Form: recipient address + amount in lovelace (1 ADA = 1_000_000).
- Build with the chosen SDK, sign via Lace, submit through the server proxy,
show tx hash + explorer link (`https://<network>.cardanoscan.io/transaction/<hash>`).
- Minimum practical send: 1_500_000 lovelace to satisfy min-ADA at the output.
## Debug panel — add on day 1 (do not retrofit)
Cardano SDK errors are opaque. A React toast usually swallows
`error.cause` and screenshots come back with a one-line
`TypeError: …` that tells you nothing. Print the full unwrapped chain to
the DOM when `?debug=1` is in the URL.
```tsx
// src/components/ErrorPanel.tsx
export function ErrorPanel({ err }: { err: unknown }) {
const unwrap = (e: any, depth = 0): string => {
if (!e || depth > 5) return "";
const head = " ".repeat(depth) + (e.name ?? "Error") + ": " + (e.message ?? e);
return head + "\n" + (e.stack ?? "") + "\n" +
(e.cause ? unwrap(e.cause, depth + 1) : "");
};
return (
<pre className="text-xs bg-black/80 text-red-300 p-3 overflow-auto max-h-80 whitespace-pre-wrap">
{unwrap(err)}
</pre>
);
}
```
Render it on every submit page behind `new URLSearchParams(location.search).has("debug")`.
This one panel will save you a day.
## 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
$5B
dance education market
SAM
$600M
online dance training resources
SOM
$50M
technique tutorial distribution
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
event access control
Dance NFT Tickets
Sell and verify exclusive dance event tickets as tradable NFTs on-chain to prevent fraud.
movement authenticityMoveProof Ledger
Record and verify unique dance moves proof-of-existence onchain to counter plagiarism.
rights marketplaceChoreo Licensing Hub
Create a decentralized marketplace for buying and selling choreography licenses directly onchain.
digital identityDanceStep Identity
Create verifiable digital identities for dancers' skills and experiences stored onchain.