Contents

Scope & non-goals

What economy-lab deliberately does not do: no payment provider, no event consumer; the host supplies those edges.

Source src/ports.ts#L216-L232Processorsrc/ports.ts#L205-L208Dispatchersrc/ports.ts#L270-L283Ratestest/conformance/invariants.adversarial.test.ts

API ProcessorDispatcherRates

Where the lab draws the line

economy-lab is a study of one thing: the application layer of a credits economy, and the invariants that hold it together.

The ledger invariants (conservation, no-overdraft, backing, chain continuity) are enforced to a production standard, pushed into the database and attacked by an adversarial suite that writes violating rows directly rather than trusting the app. The operational edges around that ledger are deliberately stubbed or simplified.

This page is the map of that second half: the things economy-lab leaves to a host, and why each one sits outside the lab.

No real payment provider

economy-lab never moves real money. When a seller cashes out, the credits retire on the ledger, but the actual disbursement to a bank or card happens somewhere the lab doesn’t reach.

That somewhere is a port. The Processor interface is the single seam through which all money leaves the platform, and it has exactly one method: submitPayout. The core asks the provider to send money and reads back a providerRef; it never learns “did it settle?” by polling. Settlement arrives later, as a webhook the provider sends back.

The reference adapter (src/adapters/processor.ts) is an HTTP client that POSTs a payout request to whatever endpoint you configure with PROCESSOR_URL. With no URL set, the host falls back to the dev stub, which approves every payout. Neither one is a money-transmitter integration.

No event consumer

When an operation finishes, some emit a domain event: a sale completed, a payout settled. economy-lab writes that event to an outbox table in the same transaction as the money move, so it can’t be lost or double-sent. Then a relay sweep ships it onward through a Dispatcher.

But the thing that receives the event is not part of economy-lab. The Dispatcher is a one-function port that hands an event off for delivery; the SQS or HTTP adapter behind it sends it to your endpoint: an internal bus, a broker, a webhook receiver.

The same is true in reverse for inbound provider callbacks. economy-lab verifies and applies them, but it does not host the messaging fabric they ride on. With no dispatcher configured, events simply stay in the outbox, undelivered, and nothing leaves the process.

None of the money-transmitter plumbing

A real credits economy sits on top of a regulated money transmitter, and that transmitter supplies a layer of plumbing that economy-lab assumes a host has already handled.

That plumbing is the compliance and settlement machinery a custodian is legally required to run:

  • KYC: know-your-customer identity verification.
  • AML: anti-money-laundering monitoring.
  • Sanctions screening: checking parties against restricted lists.
  • Payout rails: the actual bank and card connections that disburse funds.

economy-lab models a userId as an opaque usr_-prefixed token and stops there. It never sees a real identity, never screens a transfer, and never touches a payout rail. Those are the host’s responsibility, on the regulated side of the line; the lab studies the application layer above it.

Simplified schema migrations

The database schema is real and the invariants live inside it, but the migration story is built for a throwaway lab, not a system of record.

make db-migrate resets by dropping the schema and rebuilding it. The SQL drops every table and stored routine up front, so re-running it starts clean. That is the right move for a database you can recreate at will, and the worst possible move for one holding real balances: a single migration would erase the ledger.

A production custodian needs versioned, forward-only migrations that never destroy committed history. economy-lab doesn’t ship that, because the question it asks (can the engine enforce the invariants natively) doesn’t depend on it.

Concurrency at lab scale

economy-lab takes concurrency seriously where it touches correctness. A linearizability harness oversubscribes concurrent spends and checks that every committed interleaving replays serially to identical balances, and the per-account row locks keep two operations from racing on a balance.

What it does not chase is throughput at scale. There’s no cross-node partitioning of the ledger and no connection-pool tuning for thousands of writers; the one scale lever is platform-account sharding, which spreads the hot platform rows inside the same database. The concurrency work proves the invariants hold under contention; it doesn’t claim the single-database design would carry a production load.

Fixed rates, no live FX

Credits convert to dollars at fixed, platform-set rates, never a live market. The Rates port supplies the buy/par/payout rates as business constants a deployment configures.

The reference adapter (src/adapters/rates.ts) reads three integers from config and returns them; the only currencies are CREDIT and USD, and any other pair is treated as a wiring bug that throws. There is no foreign-exchange source, no rate that changes between two reads, no market risk to hedge. A credit is worth what the platform says it’s worth, and the spread between buying and cashing out is the platform’s margin, set once.

Fixed rates keep the money model deterministic — what makes the invariants checkable after every operation; a live feed would trade that for realism the lab doesn’t need.

See also