Inspiration

Every time I build a system that uses AI, I end up re-coding the same accounting backend: track tokens, price models, hold credit, enforce a spending limit, keep an audit trail. It's critical, it's easy to get subtly wrong, and it has nothing to do with the feature I actually set out to build. LedgerCap is that backend extracted into an online ledger API so I - and anyone metering AI or SaaS usage - can stop rebuilding it.

The sharper motivation: a spending cap is only worth anything if it holds when an autonomous agent fires many paid calls at once. A naive "read balance, decide, write" counter lets concurrent requests all read the same balance and overspend it. I wanted a cap that's provably safe under contention - which turned the project into a database problem, and made Aurora DSQL the load-bearing choice rather than just storage.

What it does

LedgerCap is a prepaid spend-control API for AI products and SaaS platforms. It answers one question before any expensive work runs: is this wallet allowed to spend this amount right now?

  • Wallet per agent / customer. Every agent or customer gets its own wallet - a first-class balance owner. You fund it, charge it, and suspend it at a threshold.
  • Hard, preventive cap. The spend decision is a predicate on a conditional UPDATE, not an app-side read. A charge that would cross the floor simply changes nothing - the expensive call is prevented, not reported afterwards.
  • Reservations. authorize → capture → void holds reserve credit when the final cost isn't known up front, so concurrent calls can't collectively over-promise the balance.
  • Append-only ledger. Every accepted movement is recorded with the resulting balance, in the same atomic statement as the debit - an audit trail, not double-entry accounting.
  • Integer money. Balances are integer nanodollars (1 nano = 1e-9 USD); the API takes amountNanos or amountCents, never floats.
  • Idempotent operations. Idempotency keys are bound to a request fingerprint, so a retried call can't double-charge or silently change an operation (at-least-once-safe).
  • Two planes, one core. A token-authed data plane (/api/v1/*) for machines and a Clerk-authed dashboard control plane (/api/*) call the same money library, so the UI and the API can never disagree about money.
  • A live consistency proof. A playground fires concurrent charges at one wallet and shows the safe path finishing exactly at the floor next to a naive version that overspends - the guarantee, demonstrated rather than claimed.

How I built it

  • Frontend & deploy: Next.js (App Router) from a v0-linked scaffold, deployed on Vercel. The @vercel/functions SDK handles AWS OIDC credentials and database-pool lifecycle - this does not use the ai package.
  • Database: Amazon Aurora DSQL over the PostgreSQL wire protocol. The whole data model is built around DSQL's optimistic concurrency: one hot balance row per wallet (owner_id) as the concurrency boundary; the charge is a single writable CTE that reads the live limit, conditionally debits, and inserts the ledger row atomically.
  • Concurrency: OCC conflicts surface as SQLSTATE 40001 and are retried with bounded exponential backoff; an exhausted retry budget maps to HTTP 429 (retryable), never a
    1. Unique-violations (23505) are deliberately not retried - writes are designed so concurrent same-key callers conflict on the hot row, not a primary-key insert.
  • Security: No static AWS keys ship in the app. At runtime the function assumes an IAM role via OIDC and mints a short-lived DSQL auth token for a least-privilege database role.
  • Validation & shape: Zod schemas shared between client and server; routes stay thin (auth → validate → call lib → shape JSON) with all business logic and SQL in the lib.
  • Testing: k6 load tests drive the charge path under contention, plus live probes and API verification against a real DSQL cluster after each deploy.
  • Process: I set the product model and the correctness invariants; most implementation ran through a repository-aware AI harness in scoped phases - executable checks, adversarial and Copilot review, automatic Vercel deploys, then probes against real DSQL.

Diagram

Challenges I ran into

  • Making the cap correct under concurrency. The core insight was to make the spend decision a predicate on the UPDATE rather than a preceding read, so every competing charge conflicts on the same row and DSQL's OCC serializes them - then retry 40001 and re-evaluate against the freshly committed balance.
  • DSQL is PostgreSQL-compatible, not PostgreSQL. No foreign keys (referential integrity lives in the app layer, so balances are keyed by an opaque owner_id); ALTER TABLE ADD COLUMN can't take a DEFAULT (nullable columns + COALESCE); indexes must be created ASYNC and are eventually consistent during the build; and parameter arithmetic needs explicit ::bigint casts or DSQL rejects $2 - $3 as ambiguous.
  • Idempotency that survives concurrency. A replay and a refusal both look like "zero rows updated," so the cold path has to classify the result, and the idempotency design has to be OCC-safe rather than racing on an insert.
  • Vercel deploy gotchas that pass locally but fail the pipeline. Function memory in vercel.json is rejected with Fluid compute on the Hobby plan, and any DB/network side-effect at module top level runs during next build (no DSQL reachable) - both had to be designed around.
  • Money precision. Keeping everything in integer nanodollars and being careful that computed values (rate × tokens) can exceed JS's 2^53 safe-integer limit even though MAX_NANOS doesn't.

Accomplishments I'm proud of

A hard spend cap that demonstrably holds under concurrent load, with the guarantee shown live in a playground rather than asserted - and a data model that treats Aurora DSQL's consistency as the enforcement mechanism, not just a place to store rows.

What I learned

How much of "metering" is really a distributed-systems correctness problem, and how far a single strongly-consistent conditional write can take you when you stop reading-then-writing and start letting the database serialize the decision.

What's next

  • Multi-region active deployment (the app is multi-region-aware; production is currently single-region, co-located with DSQL in us-east-1).
  • Real funding rails (e.g. Stripe) behind top-ups, and open self-serve signup.
  • Observability and billing surfaces.
  • Housekeeping: remove leftover scaffold schema and grow the automated test suite beyond load tests.

Built With

Share this project:

Updates