The SetOff clearing service and CLI. Collect obligations in windows, see who really owes whom, and settle the difference.
$ setoff net corridor.csv
ASSET OBLIGATIONS GROSS SETTLED SAVED TRANSFERS
EURC 2 1350000000 50000000 96.29% 1
USDC 5 11450000000 2900000000 74.67% 2
PLAN
EURC anchor-eu → anchor-ng 50000000
USDC anchor-ng → anchor-ke 1450000000
USDC anchor-ng → anchor-us 1450000000Seven obligations between four anchors (1,145 USDC and 135 EURC gross) settle with three transfers. This repository holds:
setoff net: nets a CSV or JSON file of obligations and prints positions, the plan and the liquidity saved (--jsonfor the full result).setoff serve: a clearing service. Participants post obligations into the open window and see their side of it; the operator, or a schedule, closes the window, which nets it, archives it, and opens the next one.setoff report: ISO 20022 camt.053 statements for a closed window.setoff soroban: the calls that settle a closed window through the settlement contract. With[settlement]configured,setoff servedoes it itself.setoff authorize: a debtor's side of that, signing its authorizations with its own account key.
The netting algorithm is an independent Go implementation of setoff-engine. The tests require it to reproduce every Rust reference vector exactly, including a 200-obligation window, and CI checks the vectors are current.
go install github.com/SetOff-Org/setoff-clearing/cmd/setoff@latest
docker run --rm ghcr.io/setoff-org/setoff version # amd64 and arm64Binaries for Linux, macOS and Windows are on Releases.
cp examples/setoff.toml . # participants, keys, assets, schedule
setoff serveexamples/setoff.toml documents every setting. Each
participant has its own key; the service refuses to start if two parties share
a key, so nobody can act as anyone else.
Signing in with a Stellar account. With [sep10] configured, a participant
can skip API keys: GET /auth?account=G… returns a challenge, the participant
signs it with its account key, and POST /auth returns a bearer token valid
for an hour (SEP-10, with the signing key published at
/.well-known/stellar.toml). A participant controlled by a
Tessera threshold group signs in the same way.
| Route | Who | What |
|---|---|---|
POST /v1/obligations |
participant | {"reference", "creditor", "asset", "amount"}. The debtor is always the caller: you can only commit yourself to pay |
GET /v1/me |
participant | Your obligations, net positions and plan legs in the open window |
GET /v1/window |
any | Open window: obligation count and live netting |
POST /v1/window/close |
operator | Net, archive and open the next window |
GET /v1/windows |
any | Archived windows, newest first |
GET /v1/windows/{n} |
any | An archived window |
GET /v1/windows/{n}/camt053 |
any | camt.053 statements: your own, or all of them for the operator |
GET /auth, POST /auth |
open | SEP-10 challenge and token, with [sep10] |
GET /metrics |
open | Prometheus counters by route |
The full schema is in api/openapi.yaml.
Durability. Every obligation is journaled with fsync before it is
acknowledged. A crash mid-write drops only the unacknowledged line, and a
crash while closing can never replay an archived window into the next one.
References are unique per debtor and window, across restarts: an identical
retry gets the original id back ("replayed": true), anything else under a
used reference is a 409.
Scheduling and notifications. close_every = "1h" closes non-empty windows
on a schedule. With [webhook] set, every close is POSTed as JSON, signed with
X-SetOff-Signature: sha256=HMAC(secret, "<X-SetOff-Timestamp>.<body>") and
retried with backoff.
The settlement contract nets on chain too, but it does not need every gross
obligation. setoff soroban submits the window's netted plan: the same net
positions in at most one leg fewer than participants per asset.
$ setoff soroban --window 7 --network testnet
# window 7: 1 submit call(s), then settle
stellar contract invoke --id CCW6QC…EYQV --source operator --network testnet -- submit --obligations '[{"debtor":"GAIH3U…","creditor":"GBRPYH…","token":"CBIELT…","amount":"1450000000","reference":"5f2c…"}]'
stellar contract invoke --id CCW6QC…EYQV --source operator --network testnet -- settleEach leg's reference is derived from the window and leg number, so a batch resubmitted after a timeout is rejected by the contract rather than booked twice.
Every submit must be authorized by its debtors, so the service collects their authorizations over the API and submits the batches itself. Add:
[settlement]
rpc = "https://soroban-testnet.stellar.org"
network = "testnet"
operator_secret_env = "SETOFF_OPERATOR_SECRET" # the contract's admin
prepare_on_close = true # or POST /v1/windows/{n}/settlement- When a window closes, the service simulates each
submitbatch and records the Soroban authorization entries the contract asks of each debtor, valid forvalidity_ledgers(default 720, about an hour). - Each debtor fetches its own from
GET /v1/windows/{n}/authorizationsand posts them back signed: the whole entry, as Stellar SDKs'authorizeEntryreturn it, or for an account just the Ed25519 signature of itshash.setoff authorizedoes this with a key from the environment. A debtor that is a Tessera threshold account signs the entry through its coordinator's/v1/authorize. - The service accepts a signed entry only if it is the issued one plus a valid
signature, byte for byte. Once a batch is fully signed it simulates, submits
and waits for it, then calls
settleafter the last one.GET /v1/windows/{n}/settlementshows progress; failures are recorded there and retried on the next signature or re-prepare.
Sessions are kept in data_dir/settlement/, so a restart resumes where it left
off. scripts/testnet-settle.sh runs the whole
flow on testnet against a freshly deployed contract:
| What | Transaction |
|---|---|
Two debtors' authorizations, one submit of the netted plan |
79b3d9d6…e9d4 |
settle |
33409ebd…2c83 |
setoff report --window 7 --participant anchor-ng > window-7.camt053.xmlOne statement per participant and asset: each obligation is a booked entry and
the net position is the closing balance. [decimals] turns smallest units into
whole units; assets without an ISO 4217 code are reported under XXX and named
in the statement.
- Collecting authorizations from contract-account debtors with custom
__check_auth, which today are checked only at simulation.