👗 Fashion & Textile Design · post-consumer waste management

Textile Recycler Booker

Lovable AI runs an app that books garment recycling pickups via Dodo so brands auto-pay haulers per kilo.

Dodo Checkout· hosted one-time payment
Section · Agentic

The kernel.

full primer →

Fashion designers tap Buy for post-consumer waste management, 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 post-consumer waste management 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
Appendix · Secrets

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.

Paste into a fresh Lovable project. Make sure the key above is set first. read the build strategy →

Build "Textile Recycler Booker" 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
Lovable AI runs an app that books garment recycling pickups via Dodo so brands auto-pay haulers per kilo.
Discipline: Fashion & Textile Design (post-consumer waste management).
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
Appendix · Market

Market sizing.

TAM
$2.5T
global fashion and apparel market
SAM
$1.2B
fashion design and sourcing software
SOM
$38M
fashion waste management software

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.