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 purchasePOST /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.