Examples

Pay-per-image billing for fal.ai apps

Updated Oct 25, 2026

These examples charge your users for image generation on fal.ai with the @fal-ai/client SDK. Use meter() to charge for the megapixels you actually deliver, or charge() for a flat price per image. You set the prices; Payfer takes them from the person’s balance and pays you what’s left after its fee.

You’ll need a connected user’s paymentUserId from the quickstart, and both SDKs on your server.

Terminal
npm install payfer @fal-ai/client
server.ts
import { fal } from "@fal-ai/client"; // reads FAL_KEY
import { Payfer } from "payfer/server";

const payfer = new Payfer({ secretKey: process.env.PAYFER_SECRET_KEY! });

Charge per megapixel of generated images

Reserve the price of every requested image at full size, generate, then collect only for the images you deliver. Payfer checks the person’s balance and allowance before fal runs, and releases whatever you don’t collect.

server.ts
// What you charge per megapixel delivered, in microdollars ($0.03).
// Set this above your fal cost.
const PRICE_PER_MEGAPIXEL = 30000;

// Payfer amounts are dollar strings with up to 6 decimal places
// (whole microdollars), and at least $0.005.
const toAmount = (micros: number) =>
  (Math.max(Math.ceil(micros), 5000) / 1e6).toFixed(6);

export async function generate(
  paymentUserId: string,
  prompt: string,
  count: number,
  operationId: string,
) {
  const size = { width: 1024, height: 1024 };
  const megapixelsEach = (size.width * size.height) / 1e6;
  // The most this can cost: every image comes back at full size.
  const maxAmount = toAmount(count * megapixelsEach * PRICE_PER_MEGAPIXEL);
  const reservation = { paymentUserId, maxAmount, idempotencyKey: operationId };

  return payfer.meter(reservation, async () => {
    const { data } = await fal.subscribe("fal-ai/flux/dev", {
      input: { prompt, image_size: size, num_images: count },
    });
    // Charge only for images the safety checker didn't flag. Throwing
    // when there are none releases the reservation.
    const images = data.images.filter((_, i) => !data.has_nsfw_concepts[i]);
    if (images.length === 0) throw new Error("No images to deliver");
    // An image without dimensions is priced at the requested size.
    let megapixels = 0;
    for (const { width = size.width, height = size.height } of images) {
      megapixels += (width * height) / 1e6;
    }
    return {
      amount: toAmount(megapixels * PRICE_PER_MEGAPIXEL),
      value: images.map((image) => image.url),
    };
  });
}

If the person can’t cover the reservation, meter() throws funding_required or allowance_exceeded before fal is called. If generation fails, the reservation is released and nothing is charged. The same pattern works for video: reserve for the longest clip you allow and collect for the seconds you return.

Charge a flat price per image

For a simple price per image, charge before calling fal, so someone without funds never costs you a generation, and refund if it fails. Reusing the operation’s idempotency key on a retry returns the same payment instead of charging twice.

server.ts
// A flat price for each image.
const PRICE_PER_IMAGE = "0.02";

export async function quickImage(
  paymentUserId: string,
  prompt: string,
  operationId: string,
) {
  // Charge first, so a user without funds never costs you a generation.
  const payment = await payfer.charge({
    paymentUserId,
    amount: PRICE_PER_IMAGE,
    idempotencyKey: operationId,
  });
  try {
    const { data } = await fal.subscribe("fal-ai/flux/schnell", {
      input: { prompt },
    });
    return data.images[0].url;
  } catch (err) {
    // No image, so give the money back.
    await payfer.refund({
      paymentId: payment.id,
      idempotencyKey: operationId + ":refund",
    });
    throw err;
  }
}

Next steps