build strategy · canton / daml

Real onchain, one secret, one build.

Every mega-prompt in this repo uses the same pattern, because it's the only pattern that lets a Lovable account ship a verifiable Canton/Daml demo in one shot.

Why Canton Seaport and not Ethereum?

Seaport is a hosted Canton 3.x participant on the Canton Devnet global synchronizer. Daml gives you typed contracts, real authorisation by signatories, and a per-user party model — no MetaMask, no gas, no faucet, no chain switching. Every contract id is publicly verifiable through the JSON Ledger API v2. Full validator reference →

The recipe

recipe
# 1. No secrets to add. LOVABLE_API_KEY is auto-provisioned by Lovable.
#    Optional fallback only if you've exhausted Lovable AI credits:
#    AISA_API_KEY=...   # Settings -> Secrets

# 2. The six Canton Devnet values are baked into the prompt. No signup, no validator setup.

# 3. Copy a mega-prompt from this repo into Lovable. One paste:
#    - scaffolds the TanStack Start app
#    - writes the Daml template (with hackathon credit in a header comment)
#    - uploads the DAR to the Seaport Devnet validator (idempotent)
#    - allocates a Canton party + creates a Daml user (userId === JWT sub)
#    - exposes the contract id, updateId, and party id in the UI

# 4. Click 'Run on Canton'. The validator returns an updateId — that's your proof.

1. The Daml template — credit baked in

Every Daml module generated from a Creative Blockchain prompt carries the hackathon credit in a header comment, so provenance is part of the on-ledger source.

daml/Provenance.daml
-- daml/Provenance.daml — every module carries the hackathon credit
-- Built during the Creative AI & Quantum Hackathon
-- organised by StreetKode Fam during Indian Krump Festival 14

module Provenance where

template Provenance
  with
    author : Party
    cid    : Text
    at     : Time
  where
    signatory author

2. OIDC token, cached server-side

src/lib/canton.functions.ts
// src/lib/canton.functions.ts — server-only OIDC + JSON Ledger API
import { createServerFn } from "@tanstack/react-start";

let cached: { token: string; expiresAt: number } | null = null;

async function mintToken() {
  const now = Date.now();
  if (cached && cached.expiresAt - 60_000 > now) return cached;
  const body = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: process.env.CANTON_DEVNET_OIDC_RUNTIME_CLIENT_ID!,
    client_secret: process.env.CANTON_DEVNET_OIDC_RUNTIME_CLIENT_SECRET!,
    audience: process.env.CANTON_DEVNET_OIDC_AUDIENCE!,
    scope: process.env.CANTON_DEVNET_OIDC_SCOPE!,
  });
  const r = await fetch(process.env.CANTON_DEVNET_OIDC_TOKEN_URL!, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });
  const j = await r.json();
  cached = { token: j.access_token, expiresAt: now + j.expires_in * 1000 };
  return cached;
}

3. Exercise a choice, get an updateId

src/lib/canton.functions.ts
// Exercise a Daml choice and get the updateId back
export const publish = createServerFn({ method: "POST" })
  .inputValidator((d: { payload: string }) => d)
  .handler(async ({ data }) => {
    const { token } = await mintToken();
    const r = await fetch(
      process.env.CANTON_DEVNET_JSON_API_URL + "v2/commands/submit-and-wait-for-transaction",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          commands: {
            userId: USER_ID,            // === JWT `sub` claim
            commandId: crypto.randomUUID(),
            actAs: [PARTY_ID],
            readAs: [],
            commands: [{
              CreateCommand: {
                templateId: PROVENANCE_TID,
                createArguments: { author: PARTY_ID, cid: data.payload, at: new Date().toISOString() },
              },
            }],
          },
        }),
      },
    );
    const j = await r.json();
    return { updateId: j.transaction.updateId };
  });

Hackathon rules of thumb

  • · One mega-prompt = one build message. Don't iterate the architecture, iterate the UI.
  • · Always show the contract id and updateId in the UI — that's your proof.
  • · Keep the runtime client_secret server-only. Never import it in client code.
  • · On Devnet, set userId equal to the JWT sub claim — the validator rejects mismatches with an opaque 403.
  • · Add a "Built during the Creative AI & Quantum Hackathon — StreetKode Fam · Indian Krump Festival 14" line to your footer.