The testnet settlement service behind Settle: it turns a bill and an amount into a fee-disclosed, expiring Stellar payment that a wallet signs, then proves what happened.
Quick start · How it works · API contract · Threat model · Testnet evidence · Contributing
Settle lets a supporter fund part or all of a verified bill, paid directly to the provider. The app never holds a key, picks a destination, or decides what a bill is worth. This service is the server side of that promise:
- Quote. The supporter names a bill and an amount. The server resolves the provider's destination, converts fiat to USDC at a disclosed rate, checks the supporter's account, reserves the amount, and returns an unsigned Stellar transaction.
- Sign. The supporter's wallet signs exactly that transaction.
- Submit. The server checks the signed transaction is byte-for-byte what it built, writes the payment down, and sends it to Stellar.
- Confirm. The server re-reads the transaction from the network and compares it to the quote. Only a match produces a receipt.
Testnet only. This service refuses to start on any other network. There is no real provider directory, no market rate source and no compliance process behind it. See what is and is not real.
| Guarantee | How it is enforced | Where it is tested |
|---|---|---|
| The client cannot choose who is paid | A request carrying destination, assetIssuer, providerId, rate, fee, memo or network is refused |
tests/quotes.test.ts |
| The wallet signs only what the server built | Field-by-field checks plus a transaction-hash equality check, before anything reaches the network | tests/payments.test.ts (13 tamper cases) |
| A retry can never pay twice | Required Idempotency-Key, single-use quotes, write-ahead payment records |
tests/payments.test.ts |
| "Confirmed" means confirmed | The recorded transaction is fetched and compared to the quote; a mismatch is a failure with no receipt | tests/payments.test.ts |
| An expired transaction can never land | The transaction's own time bound equals the quote's expiry | tests/payments.test.ts |
| Two supporters cannot both claim the last of a bill | Open quotes and in-flight payments are reserved against the bill | tests/quotes.test.ts |
| Receipts cannot be quietly edited | Append-only, hash-chained ledger that fails closed on load if it does not verify | tests/ledger.test.ts |
| The docs match the code | A test fails if openapi.yaml and the router disagree |
tests/contract.test.ts |
| It works on the real network | A script settles a real testnet payment and checks balances on Horizon | scripts/e2e-testnet.ts |
Requires Node 22.18+ (Node 24 recommended, which runs TypeScript directly).
git clone https://github.com/Anasabubakar/settle-api.git
cd settle-api
npm install
cp .env.example .env # then set SETTLE_USDC_ISSUER
npm run check # typecheck + 140 testsRun it:
export SETTLE_USDC_ISSUER=<testnet USDC issuer> # required, never defaulted
npm start
curl localhost:8787/healthz
curl localhost:8787/v1/billsProve it against the real Stellar testnet (creates throwaway accounts and a self-issued test asset):
npm run e2e:testnetThe full contract, with schemas and error codes, is openapi.yaml.
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/quotes |
Verified, expiring quote and an unsigned XDR |
GET |
/v1/quotes/{quoteId} |
Read a quote, including expiry |
POST |
/v1/payments |
Submit the signed transaction |
GET |
/v1/payments/{paymentId} |
Status; polling also drives confirmation |
GET |
/v1/receipts/{receiptId} |
A hash-chained receipt |
GET |
/v1/ledger |
Ledger head and whether the chain verifies |
GET |
/v1/bills, /v1/bills/{billId} |
Server-side funded, reserved and remaining amounts |
GET |
/v1/config, /healthz |
Network configuration, liveness |
Every POST needs an Idempotency-Key. Every error has the same shape: { "error": { "code", "message", "details?", "requestId" } }.
sequenceDiagram
participant App as Settle app
participant API as settle-api
participant W as Wallet
participant S as Stellar
App->>API: POST /v1/quotes (bill, amount, supporter)
API->>S: load supporter account
API-->>App: quote + unsigned XDR (expires)
App->>W: sign exactly this XDR
W-->>App: signed XDR
App->>API: POST /v1/payments (quoteId, signed XDR)
API->>API: verify body, hash, signature
API->>API: write payment as "submitted"
API->>S: submit transaction
API->>S: fetch the recorded transaction
API->>API: compare to the quote, append receipt
API-->>App: confirmed + receipt
See docs/architecture.md for the state machines, idempotency and recovery design.
| Status | |
|---|---|
| Quote → signature check → submission → independent confirmation → receipt | ✅ Real, tested, and run against the live testnet |
| Idempotency, expiry, single-use quotes, bill reservations, crash recovery | ✅ Real and tested |
| Hash-chained receipt ledger | ✅ Real and tested |
| Provider directory | 🟡 Fixtures. Testnet accounts that belong to no provider |
| Exchange rates | 🟡 Fixtures. Illustrative values, labelled testnet-fixture, not market data |
| Authentication and identity | 🔴 Not built. Quote endpoints are unauthenticated and bounded by rate limits and bill totals |
| Storage | 🟡 A snapshot file. Fine for a pilot; production needs a database |
| Private evidence | 🟡 Encrypted local quarantine and manual redacted review for internal synthetic data; not connected to public client |
| Mainnet | 🔴 Disabled in code. Needs verified providers, a real rate source, compliance review and an audit |
| Path | Purpose |
|---|---|
src/service.ts |
Quotes, payments, confirmation, reconciliation, idempotency |
src/payload.ts |
The two checks that make "the wallet signs only what the server built" true |
src/ledger.ts |
Append-only, hash-chained receipts |
src/chain.ts |
The Stellar network behind an interface, plus the Horizon client |
src/http.ts |
Router, limits, CORS, rate limiting, one error shape |
src/money.ts |
Integer-only fiat ↔ USDC arithmetic |
data/ |
Testnet fixture providers and bills |
openapi.yaml |
The API contract |
docs/ |
Architecture, threat model, testnet evidence, container notes |
The separate private evidence service adds encrypted quarantine, a named human review step, a minimal redacted summary and an audit trail for internal synthetic-data rehearsal. The pilot runbook states what must be proven before involving real participants.
- Settlement API contract and testnet implementation
- Idempotent submission, independent confirmation, receipt ledger
- Live testnet proof
- Encrypted evidence quarantine and manual human review foundation
- Authentication and per-supporter limits
- Database storage and an observer run as its own process
- Verified provider onboarding and destination-ownership proof
- A real, audited rate source with a lock
- Security and compliance review, closed pilot, and a mainnet decision
Read CONTRIBUTING.md and follow the Code of Conduct. Do not report vulnerabilities in a public issue; see SECURITY.md.
MIT © 2026 Anas Abubakar. See LICENSE.