WhatsApp-first payments on Stellar with direct-custody wallets and voice-to-cash.
SendAm maps a WhatsApp phone number to a Stellar wallet and lets users send, receive, check balances, and request receipts from chat. The user experience hides blockchain complexity: the backend handles wallets, addresses, and settlement on Stellar so the user only ever talks to a chat assistant.
Current status: architecture refactor in progress. The project now has production-oriented module boundaries, queue scaffolding, managed-wallet abstractions, compliance models, and expanded admin surfaces. Live money movement still requires provider credentials, KYC onboarding, monitoring, and compliance review.
- WhatsApp conversational payment assistant.
- Voice note payment intents with transcription.
- Phone number as wallet identity — every user gets a Stellar wallet.
- Direct custody: keys generated and encrypted (AES-256-GCM) locally, not managed by a third-party provider.
- Stellar as the settlement layer, built for cross-border payment corridors.
- KYC, PIN verification, audit logs, limits, and risk scoring.
- BullMQ background processing for webhook, voice, receipt, and settlement jobs.
SendAm/
apps/
api/ Express backend and worker-ready modules
landing/ Vite + React public site
admin/ Vite + React admin dashboard
packages/
shared/ Shared frontend utilities and UI
The backend is organized toward:
src/
auth/
whatsapp/
wallet/
payment/
compliance/
voice/
notifications/
admin/
pricing/
blockchain/
queues/
jobs/
common/
wallet: Direct-custody Stellar adapter plus aWalletServiceabstraction for create/get wallet, send, balance, and transaction history. App code never imports the Stellar SDK directly — onlystellar.adapter.jsdoes.payment: Payment Orchestrator for quotes, fees, rail selection, transaction execution, and receipts.whatsapp: Conversational assistant for send money, receive money, balance, contacts, history, and receipts.voice: WhatsApp audio download and Deepgram transcription pipeline.compliance: KYC tiers, transaction limits, PIN verification, and risk scoring.pricing: FX/quote service hooks for ExchangeRate API and CoinGecko.queues/jobs: BullMQ processors for asynchronous webhook and voice processing.admin: Monitoring endpoints for transactions, KYC, audit logs, and system health.
POST /api/wallet/create
GET /api/wallet/:phone/balance
GET /api/wallet/:phone/transactions
POST /api/wallet/send
POST /api/pricing/quote
GET /api/compliance/kyc/:phone
POST /api/compliance/kyc/start
POST /api/compliance/kyc/:id/review
POST /api/compliance/pin
GET /api/admin/stats
GET /api/admin/users
GET /api/admin/wallets
GET /api/admin/transactions
GET /api/admin/kyc
GET /api/admin/audit-logs
GET /api/admin/system-health
GET /webhook
POST /webhook
- Frontend: Vercel
- Backend API: Railway
- Workers: Railway or another long-running worker host, not Vercel
- Database: Neon PostgreSQL with Prisma
- Redis: Upstash
- Storage: Cloudflare R2
Use apps/api/.env.example as the source of truth. The local apps/api/.env has been expanded with blank keys for Redis, R2, pricing, KYC, ramps, and voice providers so secrets can be filled in later.
The REST wallet API (
/api/wallet/*) is unauthenticated and is disabled in production unlessENABLE_WALLET_REST_API=true. Outside production it defaults to enabled for local testing. WhatsApp is the real, signature-verified surface.
For the admin app (apps/admin/.env), configure:
VITE_API_BASE_URL=http://localhost:3002/apiFor the landing app (apps/landing/.env), configure:
VITE_ADMIN_URL=http://localhost:3001For production, set VITE_ADMIN_URL to the admin dashboard subdomain, for
example https://admin.your-domain.com.
- Node.js 18 or newer
- npm
- A PostgreSQL connection string for
DATABASE_URL— either a local Postgres via Docker Compose (below) or a cloud database (e.g. Neon) - Docker, if using the local Postgres option
- WhatsApp Business Cloud API credentials for webhook testing
A docker-compose.yml at the repo root runs a local Postgres so you don't
need a Neon (or any cloud) account to get started:
docker compose up -dThen in apps/api/.env, set:
DATABASE_URL=postgresql://sendam:sendam@localhost:5432/sendam(Skip sslmode=require from the .env.example placeholder — the local
container doesn't use SSL.)
From the repository root:
npm installRun API:
npm run prisma:generate --workspace=apps/api
npm run prisma:deploy --workspace=apps/api
npm run dev:apiRun frontend apps:
npm run dev:landing
npm run dev:adminRun builds:
npm run build:landing
npm run build:admin- Support non-native Stellar assets via
changeTrust(USDC and anchor-issued assets). - Wire Smile ID or Dojah production KYC callbacks.
- Apply the Prisma migration to the Neon database and run provider-level smoke tests.
- Split background workers from the API process in deployment.
- Add automated tests for orchestrator, wallet, webhook, voice, and compliance flows.
- Add monitoring, alerting, audit review workflows, and admin RBAC.
- Build real per-user authentication for the compliance PIN and KYC-start endpoints — they're gated off in production by default (
ENABLE_WALLET_REST_API, same as the wallet REST API) until then, so they have no working production path yet.
The backend ships with a unit test suite on the built-in Node test runner (no extra dependencies), covering wallet-secret encryption, admin auth, and request validators:
npm test # from the repo root
npm run test --workspace=apps/api # equivalentlyCI also fails a PR that touches apps/api/src/** without touching apps/api/test/** (see the api-test-coverage job in .github/workflows/ci.yml). It's a crude heuristic — maintainers can override — meant to catch untested backend changes before merge.
The three apps deploy differently because of how they run:
Static Vite builds — deploy to any static host (Vercel, Netlify, Cloudflare Pages). Build command npm run build --workspace=apps/landing (or apps/admin), output in apps/<app>/dist. Set the VITE_* variables (see Environment Variables) at build time.
The API is a long-running Express server (app.listen in src/server.js), so deploy it to a persistent Node host — Render, Railway, Fly.io, or a VM — not Vercel/Lambda serverless functions as written. The WhatsApp webhook acknowledges Meta immediately and processes the message asynchronously (via BullMQ), so a serverless function that freezes after responding would drop in-flight work.
Backend deployment checklist:
- Provision a PostgreSQL database (e.g. Neon) and set
DATABASE_URL. - Run
npm run prisma:deploy --workspace=apps/apito apply migrations. - Set every required
apps/apivariable — the server fails fast at startup withoutENCRYPTION_KEY,JWT_SECRET, andADMIN_PASSWORD, and rejects unsigned webhooks in production withoutWHATSAPP_APP_SECRET. - Set
NODE_ENV=productionand aCORS_ORIGINSallowlist covering the deployed admin/landing URLs. - Point the host's health check at
GET /health(returns 503 if the database link is down). - Configure the WhatsApp Business webhook URL to
https://<api-host>/webhookwith a matchingWHATSAPP_VERIFY_TOKEN.
Landing app (apps/landing):
/ Landing page
Admin app (apps/admin):
/login Admin login screen
/ Dashboard overview
/users User table
/wallets Wallet table
/transactions Transaction table
/kyc KYC review
/audit-logs Audit logs
/system-health System health
This project is a work-in-progress platform expansion. Some hardening is already in place:
- Real backend admin authentication (HMAC-signed session tokens); the API refuses to start without
ADMIN_PASSWORDandJWT_SECRET. - Admin API routes protected server-side by the
requireAdminmiddleware. - Wallet private keys encrypted with authenticated AES-256-GCM (tamper-detecting); the server fails fast at startup without a valid
ENCRYPTION_KEY. - WhatsApp webhook POSTs verified against the
X-Hub-Signature-256header (fail-closed in production). - Inbound message idempotency to prevent duplicate transfers from webhook retries.
- KYC tiers with daily/single-transaction limits and risk scoring, enforced on every payment via the Payment Orchestrator.
- CORS restricted to a configured origin allowlist in production.
- PostgreSQL-backed rate limiting (shared across instances): per-IP on the REST API and per-sender on the WhatsApp webhook.
- The unauthenticated REST wallet API, plus
POST /api/compliance/pinandPOST /api/compliance/kyc/start(which have the same phone-number-only identity model), are all disabled in production by default (ENABLE_WALLET_REST_API); WhatsApp is the signature-verified product surface.
Still required before a real-money launch:
- Build real per-user authentication for
POST /api/compliance/pinandPOST /api/compliance/kyc/startso they can be enabled in production — right now they're only usable with the flag on, which means no user can self-serve a PIN or start KYC in production at all. - Add secure, managed secret/key management (KMS/HSM) instead of a single static
ENCRYPTION_KEYfor wallet private keys; support key rotation. - Add audit-log coverage for all sensitive admin and compliance actions, plus monitoring/alerting.
- Expand the automated test suite to cover the payment orchestrator, wallet, webhook, voice, and compliance flows.
- Replace the single shared admin password with real admin accounts and roles.
- Complete legal, compliance, KYC, AML, and custody review where required.
MIT. See LICENSE.