Archive Footage Access
Licenses archival video via Dodo so researchers secure historical footage within grant budgets.
Dodo Checkout· hosted one-time payment
Section · Agentic
full primer →The kernel.
Choreographers tap Buy for dance research, land on a Dodo-hosted checkout, and the webhook flips the unlock the moment the payment clears — no card fields, no PCI scope, no waiting.
Why this primitiveDodo Checkout hosts the card fields and 3DS flow, so the app collects money for dance research without PCI scope — one server call, one redirect, one webhook.
Kernel
a Dodo `POST /payments` call that mints a one-time hosted checkout link for a digital deliverable, then verifies the resulting `payment.succeeded` webhook (Svix-signed HMAC-SHA256) before unlocking the goods
Drives the UI as
a single Buy button that redirects to the Dodo-hosted checkout and, on return, reads a webhook-set cookie to reveal the download
Required keys.
DODO_API_KEY
Your Dodo Payments API key (test mode by default). Create one in the Dodo dashboard under Developer → API Keys.
open ↗DODO_WEBHOOK_SECRET
Signing secret from Dodo → Webhooks → Endpoints → New Endpoint. Starts with whsec_. The webhook route verifies every request with it before unlocking the download.
open ↗Add this in your Lovable project under Settings → Secrets before pasting the prompt below.
Appendix · Mega-prompt
The build prompt.
budget · 1 message
Paste into a fresh Lovable project. Make sure the key above is set first. read the build strategy →
Build "Archive Footage Access" as a ONE-SHOT Lovable build. The participant has only
5 credits — this single message must produce a working demo with no follow-ups.
Single-page TanStack Start app. Cut scope ruthlessly.
CONCEPT
Licenses archival video via Dodo so researchers secure historical footage within grant budgets.
Discipline: Dance & Choreography (dance research).
Recipe: Dodo Checkout as the single Dodo Payments surface.
Kernel: hosted one-time Dodo checkout that mints a payment link for the deliverable
UI: one Buy button that redirects to the Dodo-hosted checkout and unlocks on webhook
CRITICAL — HOW THE RETURN PAGE LEARNS THE OUTCOME
Do NOT gate the return page on a cookie set by the webhook. The webhook is a
server-to-server request from Dodo to your endpoint, so any `Set-Cookie` it
emits goes back to Dodo's server, not the buyer's browser — the return page
would hang on "Waiting for confirmation…" forever. Instead, verify status on
the return page by calling `dodo.payments.retrieve(payment_id)` (or
`dodo.subscriptions.retrieve(subscription_id)`) with the id Dodo appends to
the `return_url` query string. Webhooks are for durable side effects only
(logs, emails, DB writes) — never for unlocking the UI.
LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
- ONE TanStack Start app, ONE public route + ONE return route + ONE webhook route.
- TanStack server functions for `startCheckout` and `verifyPayment`.
- NO database, NO auth, NO Lovable Cloud, NO extra integrations.
- NO tests, NO settings screens, NO theming toggles.
- Libraries: template defaults + `zod` + `dodopayments` + `standardwebhooks`. Nothing else.
- Demo-fallback contract: every code path must render a helpful empty state
when `DODO_API_KEY` is missing, so the app boots without secrets.
STACK
- TanStack Start app, one product/marketing route + `/thank-you` return route + `/api/public/dodo-webhook`.
- Payments: Dodo Payments Node SDK, called from `createServerFn` handlers
so the API key stays server-side.
- Webhook: `/api/public/dodo-webhook` verifies the Svix signature with
`standardwebhooks` and DODO_WEBHOOK_SECRET, then performs side effects only.
- Tailwind + shadcn. Editorial look: gold accent on a dark or warm-cream background,
generous type, one strong headline, one primary action.
- Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14".
Install once: `bun add dodopayments standardwebhooks zod`.
SERVER FUNCTION (src/lib/checkout.functions.ts) — Dodo hosted checkout:
```ts
import { createServerFn } from "@tanstack/react-start";
import { getRequest } from "@tanstack/start-server-core";
import { z } from "zod";
import DodoPayments from "dodopayments";
/** Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 */
export const startCheckout = createServerFn({ method: "POST" })
.inputValidator((d) => z.object({ productId: z.string().min(1) }).parse(d))
.handler(async ({ data }) => {
const key = process.env.DODO_API_KEY;
if (!key) throw new Error("DODO_API_KEY not set");
const dodo = new DodoPayments({
bearerToken: key,
environment: process.env.DODO_MODE === "live" ? "live_mode" : "test_mode",
});
const origin = new URL(getRequest().url).origin;
const payment = await dodo.payments.create({
payment_link: true,
billing: { city: "-", country: "US", state: "-", street: "-", zipcode: "-" },
customer: { email: "buyer@example.com", name: "Buyer" },
product_cart: [{ product_id: data.productId, quantity: 1 }],
return_url: `${origin}/thank-you`,
});
return { payment_id: payment.payment_id, payment_link: payment.payment_link };
});
```
WEBHOOK (src/routes/api/public/dodo-webhook.ts) — verify signature, side effects only:
```ts
import { createFileRoute } from "@tanstack/react-router";
import { Webhook } from "standardwebhooks";
export const Route = createFileRoute("/api/public/dodo-webhook")({
server: { handlers: { POST: async ({ request }) => {
const secret = process.env.DODO_WEBHOOK_SECRET;
if (!secret) return new Response("no secret", { status: 503 });
const body = await request.text();
let event: { type?: string; data?: { payment_id?: string } };
try {
event = new Webhook(secret).verify(body, {
"webhook-id": request.headers.get("webhook-id")!,
"webhook-timestamp": request.headers.get("webhook-timestamp")!,
"webhook-signature": request.headers.get("webhook-signature")!,
}) as typeof event;
} catch { return new Response("invalid signature", { status: 401 }); }
if (event.type === "payment.succeeded") {
// Side effects only — log, email, DB write. Do NOT Set-Cookie: the buyer's
// browser never sees this response. The return page verifies via retrieve().
console.log("paid:", event.data?.payment_id);
}
return new Response("ok");
}}},
});
```
RETURN PAGE (src/routes/thank-you.tsx) — verify via `dodo.payments.retrieve`:
```ts
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useState } from "react";
import { z } from "zod";
import DodoPayments from "dodopayments";
export const verifyPayment = createServerFn({ method: "GET" })
.inputValidator((d) => z.object({ paymentId: z.string().nullable() }).parse(d ?? { paymentId: null }))
.handler(async ({ data }) => {
if (!data.paymentId) return { status: "missing" as const };
const key = process.env.DODO_API_KEY;
if (!key) return { status: "missing" as const };
const dodo = new DodoPayments({
bearerToken: key,
environment: process.env.DODO_MODE === "live" ? "live_mode" : "test_mode",
});
const p = await dodo.payments.retrieve(data.paymentId);
const raw = String(p.status ?? "").toLowerCase();
const status = ["succeeded","success","paid","completed"].includes(raw) ? "succeeded"
: ["failed","cancelled","canceled","expired"].includes(raw) ? "failed"
: ["pending","processing","requires_action","requires_payment_method"].includes(raw) ? "pending"
: "unknown";
return { status, paymentId: data.paymentId };
});
export const Route = createFileRoute("/thank-you")({
validateSearch: (s: Record<string, unknown>) => ({
payment_id: typeof s.payment_id === "string" ? s.payment_id : undefined,
}),
loaderDeps: ({ search }) => ({ paymentId: search.payment_id ?? null }),
loader: ({ deps }) => verifyPayment({ data: { paymentId: deps.paymentId } }),
component: ThankYou,
});
function ThankYou() {
const initial = Route.useLoaderData();
const { paymentId } = Route.useLoaderDeps();
const [tick, setTick] = useState(0);
const { data } = useSuspenseQuery({
queryKey: ["dodo", paymentId, tick],
queryFn: () => verifyPayment({ data: { paymentId } }),
initialData: initial,
refetchInterval: (q) => (q.state.data as { status?: string })?.status === "pending" ? 3000 : false,
});
if (data.status === "succeeded") return <div>Unlocked — deliverable revealed.</div>;
if (data.status === "failed") return <div>Payment failed.</div>;
if (data.status === "missing") return <div>No payment id — start checkout first.</div>;
return <div>Confirming payment… <button onClick={() => setTick(t => t + 1)}>Retry</button></div>;
}
```
CLIENT (src/routes/index.tsx): one Buy button that calls `startCheckout` via
`useServerFn`, then redirects `window.location.href = payment_link`. Dodo
returns the buyer to `/thank-you?payment_id=…`; that route verifies via
`dodo.payments.retrieve()` and reveals the deliverable.
USER FLOW (the entire app — nothing else exists)
1. Land on the page; the headline previews what the product does.
2. Tap the primary action; a TanStack server function calls Dodo.
3. Redirect to the Dodo-hosted checkout; buyer pays in their preferred currency/method.
4. Return to `/thank-you?payment_id=…` (Dodo appends the id automatically);
the return page calls `dodo.payments.retrieve()` server-side to verify
status and reveal the deliverable. NEVER rely on a webhook cookie —
webhooks are server-to-server and their `Set-Cookie` never reaches the
buyer's browser.
KEYS (Dodo Payments — both required for the live path):
1. `DODO_API_KEY` — Dodo API key from the dashboard (Developer → API Keys).
Test mode by default; set `DODO_MODE=live` to charge real cards.
2. `DODO_WEBHOOK_SECRET` — Signing secret from Dodo → Webhooks → Endpoints →
New Endpoint. Starts with `whsec_`. The webhook route verifies every event
with this via the `standardwebhooks` library.
The demo MUST still boot with zero secrets and render a helpful empty state
so participants can see the shell before wiring up Dodo.
CREDIT (must appear in UI footer AND as JSDoc on the server function):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Market sizing.
TAM
$500M
global archival licensing
SAM
$20M
dance history media rights
SOM
$2M
academic dance research budgets
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
sync licensing
Sync Rights Scout
Clears indie tracks via Dodo Payments so choreographers get verified performance rights.
rehearsal logisticsStudio Slot Broker
Buys off-peak studio hours via Dodo Payments so directors secure cheap rehearsal space.
choreography licensingRoutine Rights Broker
Licenses viral choreography via Dodo Payments so student studios legally perform trending routines.
talent castingFreelance Dancer Booker
Hires commercial backup dancers via Dodo Payments so movement directors instantly lock talent.