saksh-escrow
v2.0.4
Published
Production-ready escrow service — dispute resolution, SLA monitoring, role-based permissions
Readme
saksh-escrow
A production-ready escrow service library for Node.js, backed by MongoDB and the saksh-easy-wallet payment library.
Author: Susheel Kumar — [email protected] Open to freelance and full-time remote opportunities. Reach out anytime!
How it works
Escrow Flow:
- Sender initiates escrow → funds move to Escrow Admin wallet
- Escrow enters PENDING state
- Three possible paths:
- Release: Admin releases funds to Receiver (COMPLETED)
- Cancel: Admin refunds funds to Sender (CANCELED)
- Dispute: Any party raises dispute (DISPUTED)
Dispute Resolution Flow:
- DISPUTED state with evidence collection
- Escalation levels: support → management → admin
- Optional MEDIATION with assigned mediator
- Resolution: either release (to receiver) or refund (to sender)
SLA Monitoring:
- Automated cron job checks dispute age
- Auto-escalates when SLA thresholds exceeded
- Emits events for monitoring/alerting
Where it can be used
High-Trust / High-Value Use Cases:
- Freelance platforms (milestone-based payments)
- E-commerce (hold funds until delivery confirmed)
- Real estate (deposit and deed transfer)
Speed-Sensitive / High-Volume Use Cases:
- Gig economy (driver/delivery apps)
- SaaS platforms (trial-to-subscription hold)
- Gaming / NFTs (digital asset trading)
Features
- Initiate, release, and cancel escrow transactions
- Raise disputes with categorized reasons and URL evidence
- Multi-level escalation: support → management → admin
- Mediation support with an assigned mediator
- Role-based permission system (sender, receiver, mediator, admin)
- Automatic SLA compliance monitoring with event emission
- Cryptographically secure reference IDs (
crypto.randomInt) - Atomic fund transfers (MongoDB session-backed, idempotent)
- Test seed data with one command
- JSON import/export for database backup and restore
Requirements
- Node.js 16+
- MongoDB replica set (required for atomic transactions)
mongod --replSet rs0
mongosh --eval "rs.initiate()"Installation
npm install saksh-escrow saksh-easy-wallet mongooseEnvironment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| ESCROW_ADMIN_EMAIL | [email protected] | Wallet account that holds funds in escrow |
| ADMIN_EMAIL | [email protected] | Wallet admin for fee collection |
| MONGO_URI | mongodb://localhost:27017/sakshescrow | MongoDB connection string |
| NODE_ENV | development | Environment (production enables stricter validation) |
| LOG_LEVEL | info | Logging level (debug, info, warn, error) |
Put these in a .env file — never commit it.
⚠️ Important Security Warning
Never use the default ESCROW_ADMIN_EMAIL in production.
The default [email protected] is publicly known. Attackers could attempt to:
- Social engineer support to release funds
- Create fake disputes targeting this account
Always override with your own secure admin email.
Test Data & Database Utilities
Load seed data (one command)
node scripts/db.js seedThis loads data/seed.json with ready-made users, wallets, and escrows in every state:
| Email | Balance | Role |
|-------|---------|------|
| [email protected] | USD 5,000 | Typical sender |
| [email protected] | USD 3,000 | Typical receiver |
| [email protected] | USD 8,000 | Sender with active dispute |
| [email protected] | USD 1,200 | Receiver in mediation |
| [email protected] | — | Mediator |
| [email protected] | auto | Escrow admin wallet |
Pre-seeded escrows cover all six statuses: pending, completed, canceled, disputed, mediation.
All database commands
node scripts/db.js seed # Load data/seed.json (safe to run twice — upserts)
node scripts/db.js reset # Drop all managed collections
node scripts/db.js status # Show document counts per collection
node scripts/db.js export # Export to data/backup-<timestamp>.json
node scripts/db.js export my.json # Export to a named file
node scripts/db.js import my.json # Restore from any exported backupCustomise the seed data
Edit data/seed.json directly — it is plain JSON with $oid and $date extended syntax. Add users, adjust balances, create escrows in whatever state you need for your test scenario.
Quick Start
require('dotenv').config();
const mongoose = require('mongoose');
const EscrowService = require('saksh-escrow');
const SakshWallet = require('saksh-easy-wallet');
await mongoose.connect(process.env.MONGO_URI);
const wallet = new SakshWallet();
await wallet.credit('[email protected]', 1000, 'USD', 'SEED-1', 'Deposit');
const escrowService = new EscrowService('[email protected]', 'user');
const escrow = await escrowService.initiateEscrow(
'[email protected]',
'[email protected]',
500, 'USD',
'Payment for website'
);
await escrowService.releaseEscrow(escrow._id);Examples
1. Happy Path — Initiate and Release
// examples/happy-path.js
const senderService = new EscrowService('[email protected]', 'user');
const escrow = await senderService.initiateEscrow(
'[email protected]', '[email protected]',
500, 'USD', 'Web design services'
);
await senderService.releaseEscrow(escrow._id);
// Funds move: escrow admin → bobRun it: node examples/happy-path.js
2. Cancel and Refund
const escrow = await senderService.initiateEscrow(
'[email protected]', '[email protected]',
200, 'USD', 'Logo design'
);
await senderService.cancelEscrow(escrow._id);
// Funds refunded: escrow admin → alice3. Full Dispute → Mediation → Resolution
const senderService = new EscrowService('[email protected]', 'user');
const receiverService = new EscrowService('[email protected]', 'user');
const mediatorService = new EscrowService('[email protected]', 'mediator');
const adminService = new EscrowService('[email protected]', 'admin');
const escrow = await senderService.initiateEscrow(
'[email protected]', '[email protected]',
800, 'USD', 'Mobile app MVP'
);
// Raise dispute
await senderService.raiseDispute(
escrow._id,
'App missing agreed features',
'Product Not as Described'
);
// Both parties submit evidence
await senderService.submitEvidence(escrow._id, 'https://example.com/spec.pdf');
await receiverService.submitEvidence(escrow._id, 'https://example.com/delivery.zip');
// Escalate
await senderService.escalateDispute(escrow._id, 'management');
await adminService.escalateToMediation(escrow._id, '[email protected]');
// Mediator resolves
await mediatorService.resolveMediation(escrow._id, 'refund');
// carol gets her money backRun it: node examples/dispute-flow.js
4. SLA Compliance Monitor (Cron Job)
// examples/sla-monitor.js
const systemService = new EscrowService('[email protected]', 'admin');
systemService.on('disputeAutoEscalated', ({ escrowId, nextLevel }) => {
console.log(`Auto-escalated ${escrowId} to ${nextLevel}`);
// send notification to support team
});
systemService.on('slaBreached', ({ escrowId, currentLevel }) => {
console.error(`SLA breach: ${escrowId} at ${currentLevel}`);
// page on-call admin
});
await systemService.checkSLACompliance();Run it: node examples/sla-monitor.js
Schedule with cron: 0 0 * * * node /path/to/examples/sla-monitor.js
5. Listening to Events
const svc = new EscrowService('[email protected]', 'user');
svc.on('escrowInitiated', ({ escrowId, amount, currency }) =>
console.log(`New escrow ${escrowId}: ${amount} ${currency}`));
svc.on('disputeRaised', ({ escrowId, reasonCategory }) =>
notifySupport(escrowId, reasonCategory));
svc.on('slaBreached', ({ escrowId, currentLevel }) =>
pageAdmin(escrowId, currentLevel));API Reference
new EscrowService(loggedInUser, role?)
| Parameter | Type | Description |
|-----------|------|-------------|
| loggedInUser | string | Email of the authenticated user |
| role | string | 'user' (default), 'admin', or 'mediator' |
Methods
| Method | Description | Permitted Roles |
|--------|-------------|-----------------|
| initiateEscrow(sender, receiver, amount, currency, description) | Create escrow, move sender funds to admin | any |
| releaseEscrow(escrowId) | Release funds to receiver | sender, admin |
| cancelEscrow(escrowId) | Refund funds to sender | sender, admin |
| raiseDispute(escrowId, reason, reasonCategory) | Open a dispute | sender, receiver, admin |
| submitEvidence(escrowId, url) | Attach evidence URL | sender, receiver, admin |
| escalateDispute(escrowId, level) | Escalate support level | sender, receiver, admin |
| escalateToMediation(escrowId, mediatorId) | Assign a mediator | sender, receiver, admin |
| resolveDispute(escrowId, resolution) | Resolve disputed escrow | admin, mediator |
| resolveMediation(escrowId, resolution) | Resolve mediated escrow | admin, mediator |
| checkSLACompliance() | Auto-escalate overdue disputes | system job |
Valid reasonCategory values: Fraud, Service Not Received, Product Not as Described, Other
Valid resolution values: 'release' (funds → receiver), 'refund' (funds → sender)
Valid level values: 'support', 'management', 'admin'
Events
escrowService.on('escrowInitiated', ({ escrowId, senderId, receiverId, amount, currency }) => {});
escrowService.on('escrowReleased', ({ escrowId }) => {});
escrowService.on('escrowCanceled', ({ escrowId }) => {});
escrowService.on('disputeRaised', ({ escrowId, reason, reasonCategory }) => {});
escrowService.on('disputeEscalated', ({ escrowId, level }) => {});
escrowService.on('escalatedToMediation', ({ escrowId, mediatorId }) => {});
escrowService.on('disputeResolved', ({ escrowId, resolution }) => {});
escrowService.on('disputeAutoEscalated', ({ escrowId, nextLevel }) => {});
escrowService.on('slaBreached', ({ escrowId, currentLevel, date }) => {});Error Handling
try {
await escrowService.releaseEscrow(escrowId);
} catch (error) {
if (error.message.includes('not found')) {
// Escrow doesn't exist
} else if (error.message.includes('permission')) {
// User not authorized
} else if (error.message.includes('PENDING')) {
// Wrong status for action
} else {
// Fund transfer or database error
console.error('Unexpected error:', error);
}
}Escrow Model Schema
Escrow {
senderId: String (required, email)
receiverId: String (required, email)
amount: Number (required)
currency: String (required)
description: String (required)
status: pending | canceled | completed | disputed | mediation
dispute: {
raisedBy: String
reason: String
reasonCategory: String
resolved: Boolean
escalationLevel: String (support|management|admin)
escalatedAt: Date
evidence: [String]
mediatorId: String
resolution: String
}
createdAt: Date
updatedAt: Date
}Production Deployment Checklist
- [ ] Set
NODE_ENV=production - [ ] Configure MongoDB replica set (required for transactions)
- [ ] Set strong
ESCROW_ADMIN_EMAIL(not the default) - [ ] Implement rate limiting (e.g., express-rate-limit)
- [ ] Add request idempotency keys for all mutations
- [ ] Set up monitoring for SLA breach events
- [ ] Configure webhook endpoints for escrow status changes
- [ ] Run
node scripts/db.js seedonly in development - [ ] Implement database indexes (see below)
- [ ] Add health check endpoint for your orchestrator
Database Indexes
Create these indexes for production performance:
// scripts/create-indexes.js
const Escrow = require('./models/Escrow');
async function createIndexes() {
await Escrow.createIndexes([
{ status: 1 },
{ senderId: 1, status: 1 },
{ receiverId: 1, status: 1 },
{ createdAt: -1 },
{ 'dispute.resolved': 1, status: 1 }
]);
console.log('Indexes created');
}Project Structure
saksh-escrow/
├── config/
│ └── config.js Statuses, reason categories, SLA timeframes
├── data/
│ └── seed.json Ready-made test data (users, wallets, escrows)
├── examples/
│ ├── happy-path.js Initiate → release
│ ├── dispute-flow.js Full dispute → mediation → resolve
│ └── sla-monitor.js Cron-style SLA check
├── models/
│ └── Escrow.js Mongoose schema
├── scripts/
│ └── db.js seed / reset / export / import / status
├── EscrowService.js Main service class
├── PermissionManagement.js Role-based permission checks
└── index.jsFAQ
Q: Why do I need a MongoDB replica set?
A: Atomic transactions (required for fund transfers) only work with replica sets.
Q: Can I use this without saksh-easy-wallet?
A: No, it's a hard dependency for fund management.
Q: How do I handle partial refunds?
A: Not supported in v1. Create multiple escrows for milestone payments.
Q: What happens if the escrow admin loses funds?
A: The admin wallet should be a custodial account with proper backups.
Q: Is DisputeService still used?
A: No, all functionality is in EscrowService. DisputeService.js is deprecated.
Testing
# Unit tests (when implemented)
npm test
# Integration tests (requires MongoDB)
npm run test:integration
# Load testing
npm run test:loadLicense
MIT © 2026 Susheel Kumar ([email protected])
Hiring? The author is actively looking for freelance and full-time remote opportunities in Node.js, backend architecture, and fintech systems. Contact: [email protected]
