The Stellar-native settlement engine behind Mergepay.
Authentication, group & expense logic, the settlement engine, Stellar integration, treasury multisig, anchor (SEP-24) flows, and background jobs.
| Maintainer | Role | GitHub |
|---|---|---|
| Fuhad (K1NGD4VID) | Maintainer | @K1NGD4VID |
Questions and contributions are welcome — open an issue or PR, or start a discussion.
Mergepay is a Stellar-native group settlement app that turns shared spending into
transparent, auditable, low-fee on-chain payments for friends, roommates, and
small communities. This is the backend; the frontend lives in
mergepay-web.
Built on Stellar. Every settlement is a real on-chain Stellar payment: login is SEP-10 wallet auth, payments carry a
MP:<code>memo linking them to an expense, balances settle in XLM or USDC over trustlines, shared treasuries use Stellar multisig, and fiat on/off-ramp goes through SEP-24 anchors. The server never holds user keys — it builds unsigned XDRs that the user's wallet signs.🌊 Open to contributors via Drips Wave (Stellar ecosystem). Scoped, bounty-ready issues live in DRIPS_WAVE.md; see CONTRIBUTING.md to get started.
- SEP-10 — wallet-based auth; the user's public key is their identity.
- Payments + memos — every settlement is an on-chain payment carrying a
MP:<code>memo that links it to a specific expense. - Trustlines — settle in native XLM or a stable asset (USDC by default).
- Multisig — shared treasuries can require multiple signers for withdrawals.
- SEP-24 — anchor deposit/withdraw bridges fiat and Stellar.
Private keys never touch the server. The API builds unsigned transaction envelopes; the user's wallet signs them; the API validates the signed XDR against the original intent and submits it to Horizon. The only key the server holds is its own SEP-10 signing key.
┌──────────────┐
wallet ────▶ │ mergepay-web│ (Next.js)
└──────┬───────┘
│ REST + Bearer JWT
┌──────▼───────┐ ┌──────────────┐
│ mergepay-api│◀────▶│ PostgreSQL │
│ (Fastify) │ └──────────────┘
└──┬────────┬──┘
build/submit│ │ poll status
┌──▼──┐ ┌──▼─────────┐
│Horizon│ │ worker │ (settlement + anchor reconciliation)
└──────┘ └────────────┘
▲
│ SEP-10 / SEP-24
┌────┴─────┐
│ Anchor │
└──────────┘
- Node.js 20+
- PostgreSQL 14+
- A Stellar SEP-10 signing keypair (generate one below)
git clone https://github.com/mergepay/mergepay-api.git
cd mergepay-api
npm install
cp .env.example .env
# Generate a server SEP-10 signing key and paste the secret into .env
npm run gen:sep10key
# Create the database schema
npm run prisma:generate
npm run prisma:migrate # creates tables (needs DATABASE_URL)
# (optional) demo data
npm run db:seed
# Run it
npm run dev # API on :4000
npm run worker # background reconciliation worker (separate shell)See .env.example. Key ones:
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
JWT_SECRET |
Secret for signing session JWTs (12h expiry) |
STELLAR_NETWORK |
testnet or public |
HORIZON_URL |
Horizon server |
SEP10_SIGNING_SECRET |
Server's SEP-10 signing key (npm run gen:sep10key) |
WEB_URL |
Frontend origin (CORS + invite links) |
ANCHOR_HOME_DOMAIN |
SEP-24 anchor home domain (default SDF test anchor) |
ANCHOR_WEBHOOK_SECRET |
Shared secret for the anchor webhook |
STABLE_ASSET_CODE / STABLE_ASSET_ISSUER |
Stable asset for settlement |
Every route is covered by a global default limit
(RATE_LIMIT_GLOBAL_MAX / RATE_LIMIT_GLOBAL_WINDOW_MS, default 100 per
minute), plus route-appropriate overrides for endpoints with different
traffic patterns or trust boundaries:
| Route(s) | Variables | Default |
|---|---|---|
POST /auth/challenge |
RATE_LIMIT_AUTH_CHALLENGE_MAX / _WINDOW_MS |
20 / 1 min |
POST /auth/verify |
RATE_LIMIT_AUTH_VERIFY_MAX / _WINDOW_MS |
10 / 1 min |
POST /expenses/:id/settle, POST /groups/:id/settlements |
RATE_LIMIT_SETTLEMENT_CREATE_MAX / _WINDOW_MS |
20 / 1 min |
POST /settlements/:id/confirm |
RATE_LIMIT_SETTLEMENT_CONFIRM_MAX / _WINDOW_MS |
30 / 1 min |
POST /anchors/webhook |
RATE_LIMIT_ANCHOR_WEBHOOK_MAX / _WINDOW_MS |
60 / 1 min |
Limit keys are the authenticated user's internal id when available
(never a Stellar public key), or the resolved client IP otherwise —
req.ip does not trust X-Forwarded-For unless Fastify's trustProxy
option is explicitly enabled, which this app does not do by default. If
you deploy behind a reverse proxy or load balancer and want per-client
(rather than per-proxy) limiting, enable trustProxy in src/app.ts and
make sure only your proxy can reach the app directly.
The anchor webhook's rate limit is abuse protection only — it never
replaces the shared-secret (ANCHOR_WEBHOOK_SECRET) check, which remains
the actual authentication gate for that route.
By default (RATE_LIMIT_STORE=memory) counters live in each API process's
memory, which is fine for a single instance. Set RATE_LIMIT_STORE=database
to share counters across multiple instances via a small Postgres-backed
store (rate_limit_buckets table, see
src/services/rate-limit-store.ts). That store fails open: if a count
query errors (e.g. a transient database outage), the request is allowed
through rather than the whole API returning 500s — a degraded rate limiter
is preferable to a full outage. Every 429 response includes standard
Retry-After / X-RateLimit-* headers.
POST /auth/challenge builds a challenge transaction signed by the server key.
The wallet signs it; POST /auth/verify validates the signature (handling
unfunded accounts via the master key), upserts the user, and returns a JWT.
POST /expenses/:id/settle(orPOST /groups/:id/settlements) builds an unsigned payment XDR — correct source, destination, asset, amount, and aMP:<shortCode>memo — and records apendingsettlement.- The wallet signs the XDR.
POST /settlements/:id/confirmre-parses the signed XDR, validates it matches the stored intent exactly (source, single payment op, destination, asset, amount, memo) and rejects mismatches withxdr_mismatch, then submits to Horizon, stores the tx hash, and marks the expense sharesettled.
A group registers a Stellar account it created in a wallet (the API never holds
the key). Deposits are signed by the depositor; withdrawals are signed from the
treasury account and, when treasuryRequiredSigners > 1, returned in
awaiting_signatures for additional signers before submission.
POST /anchors/deposit|withdraw creates a session and fetches a SEP-10 challenge
from the anchor. The wallet signs it; POST /anchors/sessions/:id/complete
exchanges it for an anchor JWT and the interactive deposit/withdraw URL. A signed
POST /anchors/webhook updates session status; the worker also polls.
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/challenge · /auth/verify · /auth/logout |
SEP-10 auth |
| GET/PATCH | /me |
Current user |
| POST/GET | /groups · /groups/:id |
Groups |
| POST | /groups/:id/invite · /groups/join · /groups/:id/leave · /groups/:id/archive |
Membership |
| POST/GET/PATCH/DELETE | /groups/:id/expenses · /expenses/:id |
Expenses |
| POST | /expenses/:id/settle · /groups/:id/settlements · /settlements/:id/confirm |
Settlement |
| GET | /groups/:id/balances · /groups/:id/ledger |
Balances & ledger |
| POST/GET | /groups/:id/treasury/* · /treasury-transactions/:id/confirm |
Treasury |
| GET/POST | /anchors · /anchors/deposit · /anchors/withdraw · /anchors/sessions/:id/complete · /anchors/sessions · /anchors/webhook |
Anchors |
| GET | /history |
Cross-group history |
| POST/GET | /uploads/receipt · /uploads/:file |
Receipts |
All request bodies are validated with Zod; every group action checks membership
(and admin rights where required). Errors are returned as { error: { code, message } }.
npm testTests run without a database or network — Prisma and Horizon are mocked. They
cover the settlement engine (splits, net balances, greedy suggestions), money
math, SEP-10 challenge/verify, signed-XDR validation, and the auth & group routes
via app.inject.
Deploys to Render / Fly.io / Railway. Provision Postgres (Neon/Supabase/RDS),
set the env vars, run npm run prisma:deploy on release, start the API with
npm run start, and run the worker as a separate process (npm run worker:start).
Found a vulnerability? Please report it privately — see SECURITY.md. This is testnet, unaudited software; don't run it against mainnet with real funds without your own review.
See CONTRIBUTING.md. Open-source public good — issues and PRs welcome.
MIT © 2026 Mergepay contributors.