Files
skills/docs/api-stack/api/api-plugin-payment/src/schemas/money.md
2026-05-29 08:20:56 +01:00

2.4 KiB

money

Purpose

Boundary Zod schema for MoneyLike — { amount: bigint, currency: string }. Payment APIs work in minor units (cents, pence, satoshi, lamports) and must never round-trip through IEEE-754 floats because rounding losses on 0.10 + 0.20 are exactly the kind of defect that ends up in a compliance audit. JSON cannot natively encode bigint, so the schema accepts either a real bigint literal, a finite JSON-number, or a numeric string and coerces via BigInt().

The currency code accepts 3-8 uppercase alphanumeric characters covering ISO 4217 (USD, EUR, JPY), crypto (BTC, ETH), stablecoins (USDC), and Solana micro-units (LAMPORTS).

Features

  • z.preprocess-backed amount coercion handles three input shapes (bigint / number / numeric string) and refuses everything else (objects, floats expressed as strings, NaN).
  • Currency regex ^[A-Z0-9]+$ rejects lowercase, spaces, and mixed alphanumeric junk before any downstream lookup.
  • nonnegative() on the bigint prevents negative-amount intents which providers universally reject.
  • Inferred Money type drops in everywhere MoneyLike does because the structure is identical.

Usage: Basic

import { MoneySchema } from "@suaveplan/api-plugin-payment/schemas/money";

const parsed = MoneySchema.parse({ amount: 999n, currency: "USD" });
// { amount: 999n, currency: "USD" }

Usage: Advanced

const result = MoneySchema.safeParse(await c.req.json());
if (!result.success) {
    return c.json({ error: "invalid_money", issues: result.error.issues }, 400);
}
const { amount, currency } = result.data;
const intent = await provider.createIntent({ amount: { amount, currency } });

API Reference

  • MoneySchema — z.object({ amount: bigint (coerced), currency: string (3..8, [A-Z0-9]+) }).
  • Money — z.infer<typeof MoneySchema> — { amount: bigint; currency: string }.

Implementation Notes

z.preprocess is used rather than z.coerce.bigint() because the latter happily produces BigInt("12.5") → RangeError and the error message is opaque to API consumers. The custom preprocessor inspects the input shape, only forwards values that BigInt can safely convert, and lets the downstream z.bigint() issue produce a clean failure message. Floating-point inputs are intentionally rejected — a fractional amount in minor units is a programming error, not a roundable value.