Where Open Source Meets Finance.
MergeFi lets sponsors fund open-source work, lets maintainers attach real money to GitHub issues, and pays contributors automatically the moment their pull request is merged. GitHub stays the source of truth for code and review; Stellar/Soroban smart contracts hold the money in escrow and release it without anyone having to chase an invoice.
- Contributors discover paid issues, build an on-chain-verifiable reputation, and get paid in stablecoins (USDC) or XLM the moment their PR merges.
- Maintainers create projects, attach rewards to issues or whole milestones, and approve completed work.
- Sponsors fund repos, issues, or milestones, and get a dashboard of spend, impact, and contributor performance.
This repository is the backend API. The Soroban smart contracts themselves
live in the sibling repo mergefi-contracts; this service is the
orchestration/API layer that talks to those contracts over Stellar/Soroban
RPC and mirrors on-chain state into Postgres for fast reads.
- Architecture
- Modules
- Data model
- Environment variables
- Escrow / Soroban integration
- API overview
- Setup, run, test
- Roadmap
┌────────────────────┐
│ GitHub (REST + │
│ webhooks) │
└─────────┬──────────┘
issues/PRs│ ▲ webhook (pull_request)
│ │ (HMAC-SHA256 verified)
▼ │
┌──────────────────────────────────────────────────────────┐
│ MergeFi Backend (NestJS) │
│ │
│ auth ─── users ─── github (sync + webhooks) ─── bounties │
│ │ │ │
│ │ ▼ │
│ teams ◄───┘ escrow │
│ │ │ │
│ milestones ───────────┘ │ │
│ maintenance-pool ──────────┘ │
│ sponsors / reputation / analytics│
│ │
│ Postgres (TypeORM) │
└───────────────────────────────┬──────────────────────────────┘
│ Soroban RPC / Horizon
▼
┌───────────────────────────────┐
│ Escrow smart contract(s) │
│ deployed from mergefi-contracts│
│ (Stellar / Soroban network) │
└───────────────────────────────┘
Design principles:
- GitHub is the source of truth for code. We import repos/issues/PRs via
the REST API (
@octokit/rest) and keep a local mirror for fast querying, refreshed by sync calls and kept current by webhooks. - Stellar/Soroban is the source of truth for money. Every escrow state
change (fund/release/refund/split) is a real contract invocation through
@stellar/stellar-sdk's Soroban RPC client; Postgres holds an audit-trail mirror (escrows,payments) of that on-chain activity, not the money itself. - Config-driven, not hardcoded. There is no deployed escrow contract for
this MVP session, so
ESCROW_CONTRACT_ID/TREASURY_SECRETare read from env and the Soroban client transparently falls back to a "dry run" mode when they're absent — see Escrow / Soroban integration.
| Module | Responsibility |
|---|---|
auth |
GitHub OAuth login (passport-github2) and JWT session issuance/validation (passport-jwt). |
users |
User + linked GithubAccount records, role management, Stellar address linking. |
github |
Repository/issue sync via Octokit (github-sync.service.ts) and inbound webhook handling with HMAC-SHA256 signature verification (github-webhooks.service.ts, webhook-signature.util.ts). On a merged PR, resolves the linked issue → bounty and triggers escrow release. |
bounties |
Paid-issue lifecycle: create, fund, claim, review, merge, pay, refund, expire. State machine in bounty-state-machine.ts. |
escrow |
Orchestrates fund/release/split-release/refund against the escrow contract via SorobanClientService, and keeps Escrow/Payment rows in sync. |
teams |
Team bounties: create a team with percentage splits (e.g. frontend 40 / backend 40 / testing 20), assign it to a bounty, validated to sum to 100%. |
milestones |
Fund an entire milestone's budget up front; distribute it incrementally as issues resolve (resolveIssue), splitting the remaining budget across still-open issues. |
maintenance-pool |
Recurring sponsor deposits into a shared pool; maintainers assign rewards out of the running balance for maintenance-type work. |
sponsors |
Sponsor dashboard: active bounties, total spend, budget locked in escrow, milestone progress, recent payments. |
reputation |
Computes and snapshots per-contributor stats: earnings, merged PR count, completion rate, avg review time, on-time delivery %, languages, orgs. |
analytics |
Lifetime earnings, repo/org counts, merge rate, review time, languages, a payout heatmap, and top clients (sponsors) per contributor; a platform-wide summary for the homepage. |
Cross-cutting: config (typed @nestjs/config configuration + .env),
common/entities (all TypeORM entities + enums), Swagger mounted at
/api/docs.
Entities (src/common/entities):
- User — platform account;
roles(contributor/maintainer/sponsor can overlap), optional linkedstellarAddress. - GithubAccount — 1:1 with User; OAuth tokens (never serialized out).
- Repository — synced GitHub repo metadata.
- Issue — synced GitHub issue; flagged
isMaintenanceTypefor pool-eligible work; optionally attached to a Milestone. - Bounty — 1:1 with Issue; amount/asset/difficulty/deadline/status, optional Team for split payouts, optional 1:1 Escrow.
- Escrow — local mirror of one on-chain escrow instance (owns the FK to whichever of bounty/milestone/maintenance-pool it backs), contract tx hashes, status.
- Payment — one payout leg per escrow release (one row per team-split recipient, or one for a plain release).
- Team / TeamMemberSplit — named group + percentage splits (must sum to 100).
- Milestone — budget + asset for a whole release; tracks
distributedvsbudget. - MaintenancePool — recurring balance;
monthlyDeposit, runningbalance. - ReputationSnapshot — point-in-time computed contributor stats, appended over time.
- WebhookEvent — audit log of every inbound GitHub webhook (verified or not), for replay/debugging.
See .env.example for the full annotated list. Highlights:
| Variable | Purpose |
|---|---|
DATABASE_URL |
Postgres connection string (TypeORM). |
JWT_SECRET, JWT_EXPIRES_IN |
Session token signing. |
GITHUB_CLIENT_ID / _SECRET, GITHUB_OAUTH_CALLBACK_URL |
GitHub OAuth login app. |
GITHUB_API_TOKEN |
Token used by Octokit for repo/issue sync (PAT for now; see roadmap). |
GITHUB_WEBHOOK_SECRET |
HMAC-SHA256 secret configured on the GitHub webhook. |
STELLAR_NETWORK, HORIZON_URL, SOROBAN_RPC_URL, STELLAR_NETWORK_PASSPHRASE |
Stellar network config. |
ESCROW_CONTRACT_ID |
Deployed escrow contract ID from mergefi-contracts. Not set in this environment — see below. |
MAINTENANCE_POOL_CONTRACT_ID |
Optional separate contract for the maintenance pool; falls back to ESCROW_CONTRACT_ID. |
TREASURY_ADDRESS / TREASURY_SECRET |
Platform signer used to submit release/refund transactions. |
USDC_ASSET_CODE / USDC_ASSET_ISSUER |
Stablecoin asset identity on Stellar. |
src/escrow/soroban-client.service.ts wraps @stellar/stellar-sdk's
rpc.Server to build, simulate, sign, and submit Soroban contract
invocations (fund / release / split_release / refund) against the
escrow contract. src/escrow/escrow.service.ts is the orchestration layer:
it calls the client, then persists Escrow/Payment rows and drives the
Bounty/Milestone/MaintenancePool state alongside it.
Current limitation: there is no live deployed escrow contract available
in this environment (mergefi-contracts is a separate repo/session). When
ESCROW_CONTRACT_ID or TREASURY_SECRET are unset, SorobanClientService
transparently short-circuits into a deterministic dry-run mode — it logs
a warning, returns a synthetic tx hash, and skips the network call — so the
rest of the system (state transitions, DB writes, split-percentage math,
webhook-triggered releases) can still be exercised end-to-end in tests and
local dev. Once real contracts are deployed:
- Set
ESCROW_CONTRACT_ID(andMAINTENANCE_POOL_CONTRACT_IDif separate). - Set
TREASURY_ADDRESS/TREASURY_SECRETto a funded Stellar account. - Confirm the contract's
fund/release/split_release/refundfunction signatures match the ones documented at the top ofsoroban-client.service.ts(adjust argument encoding there if not — TODOs are marked inline).
No private keys for end users are ever stored — only the platform treasury signer, and only as an env var for this MVP (see Roadmap: move to KMS/multi-sig).
All routes are mounted under /api (see app.setGlobalPrefix in main.ts).
Full interactive documentation, generated from the same decorators as the
code, is served at:
/api/docs
Route groups: /api/auth, /api/users, /api/github,
/api/github/webhooks (unauthenticated, HMAC-verified), /api/bounties,
/api/escrow, /api/teams, /api/milestones, /api/maintenance-pools,
/api/sponsors, /api/reputation, /api/analytics.
Every fund/claim/release/refund mutation — POST /bounties/:id/fund,
/claim, /refund; /escrow/fund, /escrow/:id/release,
/escrow/:id/split-release, /escrow/:id/refund; /milestones/:id/fund,
/milestones/:id/issues/:issueId/resolve; /maintenance-pools/:id/deposit,
/maintenance-pools/:id/assign-reward — requires a client-supplied
Idempotency-Key header (a UUID) so a retried request after a dropped
connection can't double-execute:
- A repeated request with the same key (scoped per endpoint and, once a route requires auth, per caller) returns the original response without re-running the mutation. 2xx and 4xx outcomes are cached and replayed verbatim; a 5xx is not cached, so a retry after a genuine server error runs cleanly rather than replaying the failure forever.
- A second request reusing a key that's still mid-flight gets
409 Conflictinstead of racing into a duplicate execution. If the original request crashed before completing, the key is reclaimed after 30s so it doesn't 409 forever. - Cached keys are retained for 24h (
IDEMPOTENCY_KEY_TTL_MSinIdempotencyInterceptor) and swept hourly byIdempotencyCleanupService.
See src/common/idempotency/ for the interceptor, decorator, and cleanup
job.
You can run this project either natively on your host machine or completely containerized using Docker.
This is the easiest way to get up and running with a pre-configured, isolated PostgreSQL database.
# A. Configure Environment Variables
cp .env.example .env
# B. Start App and Database services
docker compose up --build- Hot Reloading: The codebase is mounted into the container using a bind mount. File changes on the host will automatically trigger application restarts.
- Node Modules Isolation: The container uses an anonymous volume for
/usr/src/app/node_modules. This prevents Windows/Host compiled node packages from contaminating the Linux-native container. - Services:
- The API is served at
http://localhost:3000/api(Swagger docs athttp://localhost:3000/api/docs). - PostgreSQL is mapped to port
5432on your localhost with credentialspostgres:postgresand database namemergefi.
- The API is served at
If you prefer to run NestJS directly on your host machine:
# A. Install Dependencies
npm install
# B. Spin up only the Database service in Docker
docker compose up -d db
# C. Configure Environment Variables
cp .env.example .env
# Set DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mergefi
# D. Start NestJS in development mode
npm run start:devThe Dockerfile is structured as a secure, multi-stage build running on Alpine Linux.
docker build --target runner -t mergefi-backend:latest .- Least Privilege: The container runs under the non-root
nodeuser (USER node). - Production Mode: The final image forces
NODE_ENV=production. - JWT Secret Enforcer: If the application boots in production with the default
JWT_SECRET=insecure-dev-secret(or is missing entirely), the startup hook insrc/main.tswill crash the container. You must provide a secure customJWT_SECRETwhen running the production container:docker run -p 3000:3000 -e JWT_SECRET="your-highly-secure-random-jwt-key" mergefi-backend:latest
Unit tests and end-to-end tests are fully supported:
# Run unit tests natively
npm test
# Run unit tests with code coverage
npm run test:cov
# Run End-to-End (E2E) tests (requires PostgreSQL to be running on DATABASE_URL)
npm run test:e2eUnit tests cover critical domains including:
src/bounties/bounty-state-machine.spec.ts— the bounty lifecycle state machine.src/teams/team-split.util.spec.ts— team payout split percentage math.src/escrow/escrow.service.spec.ts— escrow fund/release/split-release/refund orchestration (Soroban client mocked).src/github/webhook-signature.util.spec.ts— GitHub webhook HMAC-SHA256 signature verification.src/github/github-webhooks.service.spec.ts— webhook-to-escrow release logic.src/bounties/bounties.service.spec.ts— bounty core management.src/sponsors/sponsors.service.spec.ts— sponsor dashboard aggregate queries (budgetLocked/totalSpend read the Escrow/Payment ledger directly).src/database/escrow-fk-integrity.integration.spec.ts— integration test against a real Postgres (requiresDATABASE_URL, not mocked): the exactly-one-parent CHECK constraint onescrows, and that sponsor dashboard figures survive a parent bounty/milestone being deleted.
DATABASE_SYNCHRONIZE=true (set in development) will auto-create tables from entities for fast local iteration. Real deployments should run migrations instead — see src/database/migrations/ and the migration:* npm scripts below.
Schema changes are tracked as TypeORM migrations under src/database/migrations/, driven by the DataSource in src/database/data-source.ts:
# Generate a migration from entity changes (requires DATABASE_URL pointed at a real DB to diff against)
npm run migration:generate -- src/database/migrations/SomeChange
# Run all pending migrations
npm run migration:run
# Revert the most recently applied migration
npm run migration:revert-
Wire up TypeORM migrations (currently relies onSeesynchronizefor local dev only).src/database/migrations/and the Migrations section above. - Move GitHub sync from a static PAT to a GitHub App installation-token flow for multi-org, least-privilege access.
- Deploy the real escrow contract from
mergefi-contractsand drop the Soroban dry-run fallback. - Replace the single
TREASURY_SECRETsigner with a proper signing service (KMS / multi-sig) before handling real funds. - Add a scheduled job for
BountiesService.expireOverdue()(deadline sweeps) and recurringMaintenancePooldeposits. - Add role-based authorization guards (currently JWT-authenticated but not role-scoped) to maintainer/sponsor-only endpoints.
- Add pagination/filtering to list endpoints (bounties, milestones, pools) as data volume grows.
- Weighted (not just even) per-issue milestone budget distribution.