Quickstart
Run LedgerForge locally with Docker and post a balanced transfer through the HTTP API.
Full guide in the repository: examples/quickstart/README.mdRun the local demo
You need Docker with a Compose plugin that supports up --wait, and Python 3. From a checkout of the repository:
$ git clone https://github.com/devaccuracy/ledgerforge.git$ cd ledgerforge$ ./examples/quickstart/start.sh$ ./examples/quickstart/transfer.shsource: -1250 unitsdestination: 1250 unitstotal debits = total credits = 1250 unitsreference retry: balances unchangedstart.sh builds the current checkout, generates a random master key into the ignored, mode-0600 file examples/quickstart/ledgerforge.local.json, runs migrations and binds the API to 127.0.0.1:5001. PostgreSQL and Redis publish no host ports. transfer.sh creates a ledger and two USD balances, posts 12.50 synchronously, checks that total debits equal total credits, then retries the same reference and confirms nothing changed.
LEDGERFORGE_API_PORT for both scripts. Stop the stack with docker compose -p ledgerforge-demo -f examples/quickstart/compose.yaml down; add -v to discard its database. The credentials and database defaults are for local use only.Make the same requests yourself
Every request carries the X-LedgerForge-Key header. For the demo, the master key is server.secret_key in the local config; in your own deployments use scoped keys for applications. The responses below are real output from a local server.
$ curl -s -X POST http://127.0.0.1:5001/ledgers \ -H "X-LedgerForge-Key: $LEDGERFORGE_KEY" \ -H 'Content-Type: application/json' \ -d '{"name": "Wallets"}'{ "ledger_id": "ldg_e04e431f-8a27-4b19-8744-eb1867ed7b23", "name": "Wallets", "created_at": "2026-10-10T05:54:45.683367002Z", "meta_data": null}Create two balances in that ledger, one for each side of the transfer:
$ curl -s -X POST http://127.0.0.1:5001/balances \ -H "X-LedgerForge-Key: $LEDGERFORGE_KEY" \ -H 'Content-Type: application/json' \ -d '{"ledger_id": "<ledger_id>", "currency": "USD", "precision": 100}'{ "currency_multiplier": 100, "balance": 0, "version": 0, "inflight_balance": 0, "credit_balance": 0, "inflight_credit_balance": 0, "debit_balance": 0, "inflight_debit_balance": 0, "ledger_id": "ldg_e04e431f-8a27-4b19-8744-eb1867ed7b23", "identity_id": "", "balance_id": "bln_e0f81b15-bd0a-43c5-9e84-7fd4394192e8", "currency": "USD", "created_at": "2026-10-10T05:54:45.686498326Z", "inflight_expires_at": "0001-01-01T00:00:00Z", "meta_data": null, "track_fund_lineage": false, "allocation_strategy": "FIFO"}Post the transfer. precise_amount is an integer in units of the given precision, so 1250 at precision 100 is 12.50. skip_queue: true waits for execution. The bounded overdraft lets the empty demo source take a debit position; it does not represent real funding.
{ "source": "bln_e0f81b15-bd0a-43c5-9e84-7fd4394192e8", "destination": "bln_df56a248-7d7e-4c0c-8fbf-a5215c87b603", "currency": "USD", "precise_amount": 1250, "precision": 100, "reference": "order-1042", "description": "Order 1042", "skip_queue": true, "allow_overdraft": true, "overdraft_limit": 12.5}$ curl -s -X POST http://127.0.0.1:5001/transactions \ -H "X-LedgerForge-Key: $LEDGERFORGE_KEY" \ -H 'Content-Type: application/json' \ -d @transfer.json{ "rate": 0, "precise_amount": 1250, "amount": 12.5, "amount_string": "12.5", "precision": 100, "overdraft_limit": 12.5, "transaction_id": "txn_c493a7e3-ac39-4456-90c3-ac43f00c5e0d", "parent_transaction": "", "source": "bln_e0f81b15-bd0a-43c5-9e84-7fd4394192e8", "destination": "bln_df56a248-7d7e-4c0c-8fbf-a5215c87b603", "reference": "order-1042", "currency": "USD", "description": "Order 1042", "status": "APPLIED", "hash": "9b7d7ad924e2e626055e3b33edcf194142e3fcd562de1807dd738d9afd7c1f87", "allow_overdraft": true, "inflight": false, "skip_queue": true, "atomic": false, "created_at": "2026-10-10T05:54:45.691597464Z", "effective_date": "2026-10-10T05:54:45.691597464Z", "scheduled_for": "0001-01-01T00:00:00Z", "inflight_expiry_date": "0001-01-01T00:00:00Z", "inflight_commit_date": "0001-01-01T00:00:00Z", "meta_data": { "allow_overdraft": true }}The source balance is now -1250 and the destination 1250:
{ "currency_multiplier": 0, "balance": 1250, "version": 2, "inflight_balance": 0, "credit_balance": 1250, "inflight_credit_balance": 0, "debit_balance": 0, "inflight_debit_balance": 0, "ledger_id": "ldg_e04e431f-8a27-4b19-8744-eb1867ed7b23", "identity_id": "", "balance_id": "bln_df56a248-7d7e-4c0c-8fbf-a5215c87b603", "currency": "USD", "created_at": "2026-10-10T05:54:45.688979Z", "inflight_expires_at": "0001-01-01T00:00:00Z", "meta_data": null, "track_fund_lineage": false, "allocation_strategy": "FIFO"}Send the same request again and the reference is rejected; the balances stay as they were:
{ "error": "reference has already been used", "error_detail": { "code": "TXN_DUPLICATE_REFERENCE", "message": "reference has already been used" }}Run the published image
The repository's docker-compose.yaml runs the server, workers, PostgreSQL and Redis from the published image. It requires a config file and binds published ports to loopback. Pin the release tag rather than :main:
$ cp ledgerforge.example.json ledgerforge.json# Replace server.secret_key first, for example with: openssl rand -hex 32$ LEDGERFORGE_IMAGE=ghcr.io/devaccuracy/ledgerforge:1.0.0 docker compose up -dThe example config sets server.secure: true, which turns on API authentication. Keep ledgerforge.json private and out of version control. Read the deployment page before exposing anything beyond your machine, and see the quickstart README for the refund and verification contract checks.