Skip to content

Quickstart ​

This gets a fiat-to-crypto backend live on Cloudflare Workers, with D1 for order storage and a cron trigger to settle orders — the recommended setup. You call createFiat(...) once inside the Worker and get the full /quote → /order flow.

Prefer Next.js / Vercel? The same handler runs there too — see Deploy. This page stays on Workers + D1 end to end.

Prerequisites ​

  • A Cloudflare account and wrangler (npm i -D wrangler).
  • A Transak account — an API key + API secret. Use staging credentials for local dev (Transak is the only live provider today).
  • Node 18+.

1. Install ​

In your Worker project:

bash
npm install @tokenflight/fiat @tokenflight/fiat-storage-d1
npm install -D @cloudflare/workers-types

The SDK is ESM-only; its only runtime dependency is zod.

2. Create a D1 database ​

bash
wrangler d1 create fiat

This prints a database_id — copy it into wrangler.jsonc in the next step.

3. Configure wrangler.jsonc ​

Bind the D1 database as DB, add a cron trigger for settlement, and set your CORS allowlist:

jsonc
{
  "name": "my-fiat-backend",
  "main": "src/index.ts",
  "compatibility_date": "2026-06-01",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [{ "binding": "DB", "database_name": "fiat", "database_id": "<paste-from-step-2>" }],
  "triggers": { "crons": ["*/2 * * * *"] },
  "vars": { "ALLOWED_ORIGINS": "https://app.example.com" },
}
  • DB is the D1 binding the state store reads.
  • triggers.crons drives fiat.cron() — the background job that finishes orders a webhook missed. Every 2 minutes is a good default.
  • ALLOWED_ORIGINS is a fail-closed CORS allowlist (comma-separated). Only these origins can call the browser-facing routes.

4. Write the Worker ​

env (with your D1 binding and secrets) is only available inside the handler, so build the instance there and reuse it. fetch serves the HTTP contract; scheduled runs the settlement cron.

ts
// src/index.ts
import { createFiat } from "@tokenflight/fiat";
import { transak } from "@tokenflight/fiat/providers/transak";
import { createD1FiatState } from "@tokenflight/fiat-storage-d1";

interface Env {
  DB: D1Database;
  TOKENFLIGHT_FIAT_SECRET: string;
  TRANSAK_API_KEY: string;
  TRANSAK_API_SECRET: string;
  ALLOWED_ORIGINS: string;
}

let fiat: ReturnType<typeof createFiat> | undefined;

function getFiat(env: Env) {
  if (fiat) return fiat;
  fiat = createFiat({
    basePath: "/v1/fiat",
    providers: [
      transak({
        apiKey: env.TRANSAK_API_KEY,
        apiSecret: env.TRANSAK_API_SECRET,
        environment: "staging", // switch to "production" when you go live
      }),
    ],
    state: createD1FiatState({ database: env.DB }),
    cors: {
      origins: env.ALLOWED_ORIGINS.split(",")
        .map((s) => s.trim())
        .filter(Boolean),
    },
    quoteSecret: env.TOKENFLIGHT_FIAT_SECRET,
  });
  return fiat;
}

export default {
  fetch: (req: Request, env: Env, ctx: ExecutionContext) => getFiat(env).fetch(req, { env, ctx }),
  scheduled: (_e: ScheduledEvent, env: Env, ctx: ExecutionContext) => ctx.waitUntil(getFiat(env).cron({ limit: 50 }, { env, ctx })),
};

That's the whole service: a provider (where crypto is bought), the D1 state store (where orders live), a CORS allowlist, and a quote secret (seals prices so they can't be tampered with between quote and order). See Configuration for every option.

5. Apply the D1 schema ​

@tokenflight/fiat-storage-d1 ships the schema. Apply it before serving traffic — locally now, and to the remote D1 before you deploy:

bash
# local D1 (for wrangler dev)
wrangler d1 execute fiat --local \
  --file node_modules/@tokenflight/fiat-storage-d1/migrations/0001_initial.sql

# remote D1 (before deploy)
wrangler d1 execute fiat --remote \
  --file node_modules/@tokenflight/fiat-storage-d1/migrations/0001_initial.sql

The migration is idempotent (CREATE TABLE IF NOT EXISTS), so re-running is safe. See State & storage for the schema.

6. Set secrets ​

Secrets never go in wrangler.jsonc. For production, use wrangler secret put:

bash
wrangler secret put TOKENFLIGHT_FIAT_SECRET   # any high-entropy string
wrangler secret put TRANSAK_API_KEY
wrangler secret put TRANSAK_API_SECRET

For local dev, put the same keys in a .dev.vars file (git-ignored) — wrangler dev loads it automatically:

ini
# .dev.vars
TOKENFLIGHT_FIAT_SECRET=dev-secret-change-me
TRANSAK_API_KEY=your-transak-staging-key
TRANSAK_API_SECRET=your-transak-staging-secret

7. Run it locally ​

bash
wrangler dev

The contract is now at http://localhost:8787/v1/fiat. Price a purchase — POST /quote returns one quote per configured provider, each carrying an opaque, sealed quoteId:

bash
BASE="http://localhost:8787/v1/fiat"

curl -sS -X POST "$BASE/quote" \
  -H 'content-type: application/json' \
  -H 'origin: https://app.example.com' \
  -d '{
    "fiatAmount": "100",
    "fiatCurrency": "USD",
    "outputAsset": { "chainId": 8453, "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" },
    "recipient": "0x1111111111111111111111111111111111111111"
  }'

Why the origin header?

Transak requires a browser origin and an end-user IP with every quote and order — a request missing either fails with CONTEXT_REQUIRED. Browsers send Origin automatically; from curl you pass one of your ALLOWED_ORIGINS yourself. The IP comes from the platform's CF-Connecting-IP header, which wrangler dev and deployed Workers set for you — on other hosts see Security.

The response is { "quotes": [...] } — pick one (there is one entry per provider; pass "provider" in the request to pin a single provider). Turn it into an order — hand that quote's quoteId back verbatim to POST /order:

bash
curl -sS -X POST "$BASE/order" \
  -H 'content-type: application/json' \
  -H 'origin: https://app.example.com' \
  -d '{ "quoteId": "fq1..3fkQ9…" }'

The response carries an action telling the frontend how to open checkout:

json
{
  "orderId": "fiat_order_01J…",
  "status": "pending",
  "provider": "transak",
  "action": { "type": "wrapper", "url": "http://localhost:8787/v1/fiat/widget/fiat_order_01J…" }
}

Open or iframe action.url to hand the user into the provider's checkout, then poll GET /order/{orderId} until status is terminal (completed, failed, canceled, or expired). The scheduled cron advances anything a webhook missed. Full request/response detail is in Calling the API.

8. Deploy ​

With the remote D1 migrated (step 5) and secrets set (step 6):

bash
wrangler deploy

Your contract is now live at https://my-fiat-backend.<your-subdomain>.workers.dev/v1/fiat. Point Transak's dashboard webhook at .../v1/fiat/webhook/transak so orders progress on provider events too.

What's next ​

  • Configuration — every createFiat option, in depth.
  • Calling the API — request/response for every route.
  • Widget & wrapper — render checkout and handle the bridge.
  • Jump routes — buy an intermediate asset and settle to any target via Hyperstream.
  • Deploy — the full Workers walkthrough, plus Next.js / Vercel.