---
name: lovable-cardano
description: Build Cardano dApps on Lovable using Lace (CIP-30), Preview/Preprod testnets, and Mesh SDK or Lucid Evolution. Mirrored and adapted from github.com/cardano-foundation/cardano-dev-skills for the Lovable + TanStack Start stack.
---

# lovable-cardano

A self-contained Cardano dev skill for Lovable vibe-coders. It teaches an AI
agent the conventions this stack expects so every prompt in the same project
stays consistent.

## Stack (fixed by Lovable)

- Frontend: **TanStack Start + Vite**. Do NOT introduce Next.js, Remix, or
  `react-router-dom`.
- UI: **shadcn/ui + Tailwind v4** (already installed).
- Server: **`createServerFn` from `@tanstack/react-start`** for RPC; server
  routes under `src/routes/api/` for webhooks or public APIs.
- Runtime: Cloudflare Workers (with `nodejs_compat`). Mesh and Lucid work
  fine; anything native (sharp, canvas, puppeteer) does not.

## Networks

Preview and Preprod only. Mainnet is intentionally excluded from prompts.

| | Preview | Preprod |
|---|---|---|
| Purpose | Fast, matches latest node | Mirrors mainnet parameters |
| Epoch | 1 day | 5 days |
| Magic | 2 | 1 |
| Address prefix | `addr_test1…` | `addr_test1…` |
| CIP-30 networkId | 0 | 0 |
| Faucet | https://docs.cardano.org/cardano-testnets/tools/faucet | same |

Pick one per project. Wallet, dApp, and provider (Blockfrost/Koios/Maestro)
must all be on the same network.

## Wallet: Lace (CIP-30)

Lace is IOG's official Cardano wallet. It speaks CIP-30 out of the box.

```ts
// src/lib/use-lace.ts
export async function connectLace(expected: "preview" | "preprod") {
  const lace = (window as any).cardano?.lace;
  if (!lace) throw new Error("Install Lace from lace.io.");
  const api = await lace.enable();
  const networkId = await api.getNetworkId(); // 0 = testnet, 1 = mainnet
  if (networkId !== 0) {
    throw new Error(`Lace is on mainnet. Switch to ${expected} and retry.`);
  }
  return { api, address: await api.getChangeAddress() };
}
```

Gotchas:
- Never call `lace.enable()` during SSR. Gate with `useEffect` or `<ClientOnly>`.
- Lace does not expose "which testnet" — networkId is 0 for both Preview and
  Preprod. Enforce the network by matching your provider's network to the
  selected testnet, and tell the user in the UI which testnet the dApp expects.
- After `enable()`, the returned `api` is stable for the tab session. Cache it.

## Off-chain SDK

Pick **one** per project.

### Mesh SDK (default for beginners)

```bash
bun add @meshsdk/core @meshsdk/react
```

```ts
import { BrowserWallet, MeshTxBuilder, BlockfrostProvider } from "@meshsdk/core";

const wallet = await BrowserWallet.enable("lace");
const provider = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID_PREVIEW!);
const tx = new MeshTxBuilder({ fetcher: provider, submitter: provider });
```

### Lucid Evolution (default for Aiken validator dApps)

```bash
bun add @lucid-evolution/lucid
```

```ts
import { Lucid, Blockfrost } from "@lucid-evolution/lucid";
const lucid = await Lucid(
  new Blockfrost(
    "https://cardano-preview.blockfrost.io/api/v0",
    process.env.BLOCKFROST_PROJECT_ID_PREVIEW!,
  ),
  "Preview",
);
const api = await (window as any).cardano.lace.enable();
lucid.selectWallet.fromAPI(api);
```

## Providers and secrets

Blockfrost / Maestro / Koios project IDs are **server-side secrets**.

- Add via Lovable's secrets UI as `BLOCKFROST_PROJECT_ID_PREVIEW` /
  `BLOCKFROST_PROJECT_ID_PREPROD`.
- Read them only inside `createServerFn(...).handler()` — never in module
  scope, never in browser bundles.
- Never prefix with `VITE_`.

## On-chain: Aiken

Keep validators in `onchain/`. Lovable's build container does not run
`aiken build` — commit the compiled `plutus.json` and load it from
`src/lib/plutus.ts`.

```ts
import plutus from "../../onchain/plutus.json";
export const VESTING_SCRIPT = plutus.validators.find(v => v.title === "vesting.vesting.spend");
```

Reference: https://aiken-lang.org/example--vesting/

## Transaction submission checklist

Every tx path must handle:

1. **No UTxOs** — user's wallet is empty; direct them to the faucet.
2. **Wrong network** — networkId or provider mismatch; surface a "switch Lace to
   {network}" hint.
3. **CBOR / min-ADA errors** — most SDKs handle these, but datum-carrying UTxOs
   need at least ~1.5 ADA min output.
4. **Timeout** — treat submit as fire-and-forget; poll the explorer for the
   tx hash before enabling "send again".
5. **User rejection** — Lace throws with code 2 (`User declined to sign`).

## Testnet-only banner (required in every dApp)

```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>
```

## Red flags

- Shipping a provider project ID to the browser (any `import.meta.env.VITE_*`
  Blockfrost key). Move to a server function.
- Calling `window.cardano.lace.enable()` at module scope or in an SSR loader.
- Mixing Mesh and Lucid in the same tx builder.
- Hard-coded network in the SDK config that disagrees with the wallet's
  reported network — refuse to submit if they don't match.
- Any Mainnet code path in a hackathon prompt.

## Bundled references

- Cardano Foundation official docs (LLM-friendly): https://developers.cardano.org/llms-full.txt
- Upstream skill pack: https://github.com/cardano-foundation/cardano-dev-skills
- Cardano docs: https://docs.cardano.org/
- Mesh SDK: https://meshjs.dev/
- Lucid Evolution: https://lucidevolution.io/
- Aiken: https://aiken-lang.org/
- Testnet faucet: https://docs.cardano.org/cardano-testnets/tools/faucet
