Peer-to-peer XLM ↔ Naira exchange with on-chain Stellar escrow.
Nexlm lets Nigerians buy and sell XLM directly with each other using bank transfer, OPay, PalmPay, Kuda and Moniepoint. The seller's XLM is locked in a dedicated Stellar escrow account for every trade, so neither side has to trust the other — and Nexlm never touches the Naira.
| Repository | Purpose |
|---|---|
| Nexlm/nexlm | Trading app — API + web client (this repo) |
| Nexlm/nexlm-landing | Marketing website |
| Nexlm/nexlm-docs | User, developer and API documentation |
- Accounts — email registration and verification, password reset, JWT sessions, rate-limited auth
- Stellar wallet — a keypair is generated per user (secret encrypted with AES-256-GCM), funded by Friendbot on testnet; balances, QR deposits, withdrawals and on-chain activity via Horizon
- KYC — BVN/NIN verification through Smile ID, with manual admin review as a fallback; only a keyed fingerprint and last four digits are stored
- P2P order board — buy and sell orders with price, payment methods and terms, sorted best price first, auto-expiring after 30 minutes
- Escrow trades — per-trade escrow lock, 15-minute payment window, mark as paid, release, cancel and automatic refunds on timeout
- Trade chat — realtime Socket.io chat with payment-proof image uploads (magic-byte validated, Cloudinary or local disk)
- Reputation — completed trades and completion rate on offers and public profiles
- Admin — overview dashboard, user search/suspension, KYC review, trade monitoring with read-only chat
- Reliability — atomic state transitions, reconciliation of trades whose Stellar outcome was unknown
lock seller ─ createAccount(escrow, amount + 2 XLM) ─▶ escrow
escrow ─ setOptions(add platform signer, master weight 0)
release escrow ─ payment(amount) ─▶ buyer
escrow ─ setOptions(remove platform signer)
escrow ─ accountMerge ─▶ seller (returns unused reserve)
refund escrow ─ setOptions(remove platform signer)
escrow ─ accountMerge ─▶ seller
The escrow's own key is disabled in the same atomic transaction that funds it, so only the platform co-signer can move the XLM — and the only operations it ever signs send funds to the buyer or back to the seller. Every step is a public transaction linked from the trade room.
| Layer | Technology |
|---|---|
| Frontend | React 18, Vite, Tailwind CSS, Zustand, React Router |
| Backend | Node.js, Express, Zod |
| Database | PostgreSQL, Prisma |
| Blockchain | @stellar/stellar-sdk, Horizon |
| Realtime | Socket.io |
| Auth | JWT, bcrypt |
| Uploads | Cloudinary (local disk fallback) |
| KYC | Smile ID |
Requirements: Node.js 20+, PostgreSQL 14+ (or Docker).
git clone https://github.com/Nexlm/nexlm.git
cd nexlm
npm install
# Database
docker compose up -d
# Server configuration
cp server/.env.example server/.env
node -e "console.log('ENCRYPTION_KEY=' + require('crypto').randomBytes(32).toString('hex'))"
node -e "import('@stellar/stellar-sdk').then(s => console.log('PLATFORM_SECRET_KEY=' + s.Keypair.random().secret()))"
# paste both values and a long JWT_SECRET into server/.env
npm run db:migrate
ADMIN_EMAIL=you@example.com ADMIN_PASSWORD='choose-a-strong-one1' npm run db:seed
# Run
npm run dev:server # http://localhost:4000
npm run dev:client # http://localhost:5173On testnet, new wallets are funded automatically with 10,000 test XLM. Without SMTP configured, verification and reset emails are printed to the server log.
The repo deploys as two Vercel projects from main:
| Project | Root Directory | Preset | URL |
|---|---|---|---|
nexlm-server |
server |
Other | https://nexlm-server.vercel.app |
nexlm-client |
client |
Vite | https://nexlm-client.vercel.app |
- Create a Postgres database (e.g. Neon via Vercel Marketplace).
- In
nexlm-server, setDATABASE_URL,JWT_SECRET,ENCRYPTION_KEY,PLATFORM_SECRET_KEY,CRON_SECRET,CLIENT_URL=https://nexlm-client.vercel.app,PUBLIC_API_URL=https://nexlm-server.vercel.app(plusCLOUDINARY_URLfor image uploads) and redeploy. Migrations run during the build. - In
nexlm-client, setVITE_API_URL=https://nexlm-server.vercel.appand redeploy. - Check
https://nexlm-server.vercel.app/health. If a variable is missing, the API answers withSERVER_NOT_CONFIGUREDand lists it.
On Vercel the API runs serverless: Socket.io is replaced by client polling, and expiry/refund/reconciliation jobs run on incoming traffic plus a cron endpoint. For always-on realtime, deploy the API with render.yaml instead. Full guide: docs → Deployment.
| Command | Description |
|---|---|
npm run dev:server |
API with auto-reload |
npm run dev:client |
Web client |
npm test |
Server and client test suites |
npm run lint |
ESLint across both workspaces |
npm run test:coverage -w server |
Server tests with a coverage report |
npm run build |
Production build of the client |
npm run db:migrate |
Apply Prisma migrations in development |
npm run db:seed |
Create or promote an admin |
npm run smoke -w server |
End-to-end test against a running API on testnet |
server/
prisma/ schema, migrations, admin seed
scripts/ testnet smoke test
src/
config/ env validation, constants
lib/ amounts (BigInt), crypto, errors, pagination
stellar/ Horizon client, wallets, payments, escrow
services/ auth, wallet, orders, trades, chat, KYC, admin
routes/ Express routers
middleware/ auth, validation, rate limits, uploads, errors
socket/ Socket.io auth and rooms
jobs/ order expiry, auto-refunds, reconciliation
tests/ unit tests (Vitest)
client/
src/
pages/ market, orders, trades, wallet, KYC, settings, admin
components/ UI kit, trade room, wallet, layout
store/ Zustand auth and toast stores
hooks/ lib/ API client, sockets, formatting
All routes are under /api. Machine-readable reference: docs/api/openapi.yaml
(kept in step with the router by a test). Narrative guides: nexlm-docs.
| Area | Endpoints |
|---|---|
| Auth | POST /auth/register · /login · /verify-email · /forgot-password · /reset-password · /change-password |
| Users | GET/PATCH /users/me · /users/me/payment-accounts · GET /users/:displayName |
| KYC | GET /kyc · POST /kyc |
| Wallet | GET /wallet · /wallet/deposit · /wallet/activity · /wallet/transactions · POST /wallet/withdraw |
| Orders | GET /orders · /orders/mine · /orders/:id · POST /orders · /orders/:id/cancel |
| Trades | GET/POST /trades · GET /trades/:id · POST /trades/:id/paid · /release · /cancel · GET/POST /trades/:id/messages |
| Admin | GET /admin/overview · /admin/users · PATCH /admin/users/:id/status · POST /admin/kyc/:id · GET /admin/trades |
Realtime events: order:created, order:removed, trade:created, trade:updated, message:new, trade:typing.
- Server unit tests cover amount math, encryption, validators, trade state rules, services with the database mocked, escrow transaction shapes and the background jobs.
- Route tests run the real Express app over HTTP and check status codes, validation, auth gates and error bodies.
- Client tests render components, hooks and pages in jsdom with the API mocked.
- Smoke test (
server/scripts/smoke-test.mjs) runs a full lifecycle on Stellar testnet: registration, escrow lock, chat upload, release, cancel, timeout auto-refund and withdrawal.
More: architecture · testing · onboarding · runbook · API spec
- Phase 2 — ratings and reviews, disputes with admin resolution, 2FA, notifications, analytics
- Phase 3 — partial fills, live market pricing, advanced order management, referrals, mobile app
See CONTRIBUTING.md, especially the rules for money-moving code.