Self-hostable swap service that lets anyone exchange Dogecoin or Litecoin for BSV — published open source so that anyone, anywhere, can run their own on-ramp. BSV is hard to buy in large parts of the world; one hosted swap cannot fix that, but a swap that anyone can operate in their own jurisdiction can.
No hosted instance. ORDnet does not operate a public swap service. Offering crypto exchange to the public is a licensed activity in the EU under MiCA — regardless of how small the amounts are — so this repository publishes the software only. Running it is up to you, in your own jurisdiction, under your own compliance. See Operator responsibility below.
v3.0.0 contained two critical, fund-affecting vulnerabilities: an unauthenticated SQL injection in order creation, and payouts released with zero confirmations. Both are fixed in v3.1.0. Do not run v3.0.0. Details: SECURITY-FIXES-v3.1.0.md.
v3.1.0 introduces two breaking changes —
ORDSWAP_ADMIN_KEYis now mandatory, and the setup endpoints are loopback-only by default.
- A user creates a swap order and receives a unique DOGE or LTC deposit address — HD-derived per order (BIP32/BIP44), so concurrent payments can never be confused.
- The monitor watches the deposit address. Once the payment reaches the required number of confirmations and the received amount covers what is owed, the server pays out BSV from the operator's liquidity wallet.
- Orders persist in SQLite with refreshable
/swap/:orderIdURLs.
- Payouts wait for confirmations. Default 2 for both DOGE and LTC. An unconfirmed payment can still be replaced; releasing irreversible BSV against one is a double-spend invitation.
- The received amount is verified against the amount owed, in integer atomic units, with a 1% tolerance for rounding. Short payments are held for review rather than paid out or discarded.
- Every SQL statement is parameterised. No request data and no third-party API response is ever concatenated into SQL.
- No plaintext keys at rest. The BSV liquidity WIF is stored
AES-256-GCM encrypted (
ORDSWAP_ENCRYPTION_KEY); deposit addresses are derived watch-only from xpubs — the deposit-side mnemonic can stay offline entirely (xpub-only setup, option C in the wizard). - Admin panel behind a mandatory key, compared in constant time, rate limited with lockout, and never exposed via CORS.
- Payouts are claimed atomically. Two monitor runs cannot pay the same order twice, and a payout that fails halfway is never retried automatically — it waits for a human.
npm install
export ORDSWAP_ENCRYPTION_KEY="$(openssl rand -hex 32)"export ORDSWAP_ADMIN_KEY="$(openssl rand -hex 32)"
npm start
# open http://localhost:3460/adminThe setup endpoints are loopback-only by default because they mint and accept seed phrases. To configure from another machine, tunnel:
ssh -L 3460:localhost:3460 your-serverSetup wizard: generate a wallet (write the seed phrase down!), import an existing mnemonic, or — recommended — enter xpubs only and keep keys offline. Then configure the BSV liquidity wallet, put a TLS proxy in front, and you are an on-ramp.
Required:
| Variable | Purpose |
|---|---|
ORDSWAP_ENCRYPTION_KEY | AES key for WIF-at-rest encryption |
ORDSWAP_ADMIN_KEY | Admin panel key, minimum 32 characters |
Optional:
| Variable | Default | Purpose |
|---|---|---|
ORDSWAP_PORT | 3460 | Listen port |
ORDSWAP_DB | ./ordswap.sqlite | SQLite path |
ORDSWAP_MIN_CONF_DOGE | 2 | Confirmations before a DOGE-funded payout |
ORDSWAP_MIN_CONF_LTC | 2 | Confirmations before an LTC-funded payout |
ORDSWAP_AMOUNT_TOLERANCE | 0.01 | Accepted underpayment fraction |
ORDSWAP_MIN_USD / ORDSWAP_MAX_USD | 0.25 / 2.00 | Order size range |
ORDSWAP_MARKUP | 2 | BSV price markup |
ORDSWAP_EXPIRY_MINUTES | 30 | Order lifetime |
ORDSWAP_ALLOW_REMOTE_SETUP | unset | 1 opens setup endpoints beyond loopback |
ORDSWAP_CORS_ORIGIN | * | CORS origin for public endpoints only |
ORDSWAP_CREATE_LIMIT | 10 | Orders per IP per 10 minutes |
ORDSWAP_READ_LIMIT | 240 | Public reads per IP per minute |
ORDSWAP_ADMIN_FAIL_LIMIT | 5 | Failed admin auths per IP per 15 minutes |
ORDSWAP_RATE_MAX_AGE | 900 | Seconds before exchange rates count as stale |
ORDSWAP_MAX_BODY_BYTES | 65536 | Request body cap |
Public:
GET /healthGET /ratesPOST /swap/create—{ amount_usd, pay_coin, bsv_wallet }GET /api/swap/:orderId
Admin (X-Admin-Key header): /admin/config, /admin/liquidity,
/admin/orders, /admin/stats.
Setup (X-Admin-Key header, loopback-only by default):
/admin/setup/{status,generate,import,configure}.
HD derivation: DOGE m/44'/3'/0'/0/x · LTC m/44'/2'/0'/0/x.
| status | meaning |
|---|---|
pending | waiting for payment |
seen | payment in the mempool, waiting for confirmations |
processing | confirmed, payout being built |
broadcasting | payout signed, txid recorded, broadcast in flight |
completed | done |
underpaid | confirmed but short — held for review, not paid |
payout_failed | payout errored — held for review, not retried |
expired | no payment in time |
/admin/stats exposes a needs_review counter covering the four states
that wait for a human. Watch it.
node test/security-test.mjs # 74 assertions
node --experimental-sqlite test/injection-poc.mjs # reproduces and closes K1Both run on plain Node (>= 22.5 for the PoC) with nothing installed.
Running a swap service for the public is a licensed activity in many jurisdictions — money transmission, MiCA in the EU, and comparable regimes elsewhere. Thresholds are lower than people expect: in the EU there is no de-minimis exemption that makes a one-dollar swap unregulated.
This repository publishes software. Whether and how you may operate it is your own responsibility. Find out what applies to you before you point a domain at it — not after.
Security reporting: SECURITY.md.
- ORDnet-x402 — micropayments in native sats once you hold BSV
- ORDnet-MCP-Server — the agent toolset (uses ORD/swap for agent funding)
MIT © ORDnet / ODNCA — see LICENSE.