Contest Fee Filer
Auto-pays magazine submission fees via Dodo so poets maximize their contest entries within a strict budget.
Dodo Checkout· hosted one-time payment
Section · Agentic
full primer →The kernel.
Writers tap Buy for literary submissions, 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 literary submissions 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 "Contest Fee Filer" 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
Auto-pays magazine submission fees via Dodo so poets maximize their contest entries within a strict budget.
Discipline: Writing, Poetry & Narrative (literary submissions).
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 literary magazine and contest ecosystem
SAM
$120M
poetry and short fiction submission fees
SOM
$8M
automated literary submission trackers
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
screenplay rights
Screenplay Option Broker
Auto-checkouts option agreements via Dodo Payments so screenwriters secure upfront payments instantly.
poetry publishingAnthology Rights Clearer
Negotiates and checkouts reprint permissions via Dodo Payments so poets get paid for anthologies.
manuscript consultingSensitivity Reader Booker
Discovers and checkouts sensitivity readers via Dodo Payments so authors ensure authentic representation.
nonfiction researchQuote Permission Agent
Auto-checkouts copyright permissions via Dodo Payments so biographers legally quote protected archives.