Beetles Docs

Multi-Currency Exchange

Cross-ledger currency exchange using atomically linked transfers.

Multi-Currency Exchange

When your platform handles multiple currencies, Beetles models each currency as its own ledger. Exchanges move money across ledgers using atomically linked transfers — if one leg fails, both are rolled back.

Architecture

The exchange flow uses Identities to resolve destination accounts automatically. An Identity groups accounts across ledgers under one entity (e.g. "Acme Corp" has a BRL account and a USD account).

The two transfers are atomically linked at the engine level — they succeed or fail together. There is no intermediate state where one side has moved but the other hasn't.

Entity Setup

Before executing exchanges, you need ledgers per currency and accounts linked via an Identity:

If the source account doesn't have an Identity yet, one is auto-created using the account name as label. If the destination account doesn't exist, it's also auto-created in the target ledger.

API Flow

Create Ledgers for Each Currency

BRL Ledger
curl -X POST http://localhost:8080/v1/ledgers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Beetles-Workspace-Id: $WS_ID" \
  -H "Content-Type: application/json" \
  -d '{ "name": "BRL Operations" }'
USD Ledger
curl -X POST http://localhost:8080/v1/ledgers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Beetles-Workspace-Id: $WS_ID" \
  -H "Content-Type: application/json" \
  -d '{ "name": "USD Treasury" }'
Create Identity
curl -X POST http://localhost:8080/v1/identities \
  -H "Authorization: Bearer $TOKEN" \
  -H "Beetles-Workspace-Id: $WS_ID" \
  -H "Content-Type: application/json" \
  -d '{ "label": "Acme Corp" }'
Link Account to Identity
curl -X PUT http://localhost:8080/v1/identities/$IDENTITY_ID/accounts/$BRL_ACCOUNT_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Beetles-Workspace-Id: $WS_ID"

Execute the Exchange

POST /v1/exchanges
curl -X POST http://localhost:8080/v1/exchanges \
  -H "Authorization: Bearer $TOKEN" \
  -H "Beetles-Workspace-Id: $WS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "source_account_id": "BRL_ACCOUNT_ID",
    "destination_ledger_id": "USD_LEDGER_ID",
    "amount": 542000,
    "exchange_rate": 5.42,
    "code": 1
  }'
Response (201 Created)
{
  "source_transfer": {
    "id": "01969...",
    "debit_account_id": "BRL_ACCOUNT_ID",
    "credit_account_id": "WORLD_BRL_ID",
    "amount": 542000,
    "status": "success"
  },
  "destination_transfer": {
    "id": "01969...",
    "debit_account_id": "WORLD_USD_ID",
    "credit_account_id": "USD_ACCOUNT_ID",
    "amount": 100000,
    "status": "success"
  },
  "destination_account_id": "USD_ACCOUNT_ID",
  "exchange_rate": 5.42,
  "source_amount": 542000,
  "destination_amount": 100000
}

The amount is in the source currency's smallest unit. With exchange_rate: 5.42, the destination receives amount / rate = 542000 / 5.42 = 100000 (i.e. $1,000.00).

Request Fields

FieldTypeRequiredDescription
source_account_idUUID✅Account to debit in the source ledger
destination_ledger_idUUID✅Target ledger (must differ from source)
amountinteger✅Amount in the source currency's smallest unit
exchange_ratefloat✅Units of source currency per 1 unit of destination
codeinteger✅Transfer code for categorization

Validation Rules

  • Source and destination must be different ledgers (same-ledger → 422)
  • exchange_rate must be > 0 (zero or negative → 400)
  • Source account must have sufficient balance (overdraft → engine-level rejection)
  • If no Identity exists for the source account, one is auto-created using the account name

Cross-Reference

Both transfers include a reference field that links them together:

  • Source transfer: reference: "exchange:<destination_transfer_id>"
  • Destination transfer: reference: "exchange:<source_transfer_id>"

This allows you to trace both legs of an exchange from any single transfer.

How It Works Under the Hood

  1. Validate source account + destination ledger (workspace-scoped)
  2. Resolve Identity from the source account
  3. Find or create a destination account for that Identity in the target ledger
  4. Create 2 atomically linked transfers in the ledger engine:
    • Leg 1: source_account → @world (source ledger) — debits the source
    • Leg 2: @world → destination_account (dest ledger) — credits the destination
  5. Persist both transfers

The linked flag ensures atomicity — if either leg fails (e.g. insufficient balance), both are rejected. No partial state is ever possible.

On this page