DroneFootage Token
Mint drone videos as provenance NFTs to secure unique aerial shot copyrights.
Mint an NFT· asset primitive
Section · Onchain
full primer →The primitive.
A native-script policy signs a CIP-25 metadata payload. The IPFS-hosted image becomes an on-chain asset in the connected wallet.
Kernel
A native-script policy signs a CIP-25 metadata payload. The IPFS-hosted image becomes an on-chain asset in the connected wallet.
Drives the UI as
An upload / name / description form, an in-flight minting state, and a gallery card once the tx settles.
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 · Mint an NFTA native-script policy signs a CIP-25 metadata payload. The IPFS-hosted image becomes an on-chain asset in the connected wallet.
# DroneFootage Token
> Mint drone videos as provenance NFTs to secure unique aerial shot copyrights.
_Context: Videography & Film · aerial videography_
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.
## Buffer polyfill (browser)
Cardano SDKs pull in code that assumes Node's global `Buffer`. In the
browser, polyfill it inside the submit function via **dynamic import** —
never at module scope.
```ts
export async function submitSomething() {
const { Buffer } = await import("buffer");
(globalThis as any).Buffer ??= Buffer;
// …now build/sign/submit
}
```
Do NOT do `import "buffer/"` or `import { Buffer } from "buffer"` at the
top of a module — Vite's dep optimizer will hang or ship the polyfill into
the initial bundle unnecessarily. Dynamic import in the code path that
actually needs it keeps the initial bundle small and predictable.
## App shape: Mint an NFT
- Native-script policy expiring at (currentSlot + 3600).
- Metadata under label 721 (CIP-25): `name`, `image` (ipfs:// URL), `mediaType`.
- Mint amount = 1. `policyId` derived from the wallet's payment key hash.
- **Min-ADA**: token-carrying outputs need ≥ ~1.5 ADA attached. Send at least
2_000_000 lovelace with the mint output or the tx will fail with
`OutputTooSmallUTxO`.
- After submission, poll the explorer for confirmation before enabling
"mint again".
## 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
$1.1B
drone videography and editing
SAM
$180M
professional drone videographers
SOM
$12M
freelance aerial content creators
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
licensed assets
ClipLicense Swap
Facilitate secure, onchain licensing and transfer of video clips between creators.
video authenticityContentTrust Badge
Award verifiable trust badges onchain for authentic and verified video content.
video stakingClipStake Platform
Stake tokens on promising clips to support creators and earn rewards via smart contracts.
editorial feedbackChainFeedback Loop
Collect and verify editorial feedback on video edits onchain to improve collaboration.