DEV Community

Daniel Ioni
Daniel Ioni

Posted on

πŸ” MyZubster Escrow System: Deep Dive Guide

πŸ” MyZubster Escrow System: Deep Dive Guide

Complete technical guide to multi-signature escrow – from order creation to dispute resolution
πŸ“Œ What is the Escrow System?

MyZubster's escrow system is a multi-signature smart contract that secures transactions between buyers and sellers. It ensures that:

βœ… Funds are locked until service is delivered

βœ… Buyer is protected – money is only released when satisfied

βœ… Seller is protected – guaranteed payment if service is delivered

βœ… Dispute resolution – fair arbitration for conflicts
Enter fullscreen mode Exit fullscreen mode

🧩 Architecture Overview
text

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ MyZubster Escrow System β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Order Lifecycle β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ PENDING ───► PAID ───► IN_PROGRESS ───► COMPLETED β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β–Ό β–Ό β–Ό β–Ό β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Locked β”‚ β”‚Locked β”‚ β”‚Locked β”‚ β”‚Releasedβ”‚ β”‚ β”‚
β”‚ β”‚ β”‚ XMR β”‚ β”‚ XMR β”‚ β”‚ XMR β”‚ β”‚ XMR β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Buyer β”‚ β”‚ Seller β”‚ β”‚ Arbitrator β”‚ β”‚
β”‚ β”‚ 1 key β”‚ β”‚ 1 key β”‚ β”‚ 1 key β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β–Ό β–Ό β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ 2-of-3 Multi-Signature Escrow β”‚ β”‚
β”‚ β”‚ (Funds require 2 of 3 signatures) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ› οΈ Escrow Lifecycle
The Complete Flow
text

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Buyer β”‚ β”‚ Marketplaceβ”‚ β”‚ Gateway β”‚ β”‚ Monero β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ (Escrow) β”‚ β”‚ Blockchain β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚ β”‚
β”‚ 1. Creates β”‚ β”‚ β”‚
β”‚ Order with β”‚ β”‚ β”‚
β”‚ Skill ID β”‚ β”‚ β”‚
│─────────────────▢│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 2. Request β”‚ β”‚
β”‚ β”‚ Payment β”‚ β”‚
β”‚ β”‚ (POST /initiate) β”‚ β”‚
β”‚ │──────────────────▢│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 3. Generate β”‚
β”‚ β”‚ β”‚ Subaddress β”‚
β”‚ β”‚ │──────────────────▢│
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 4. Return β”‚
β”‚ β”‚ β”‚ Subaddress β”‚
β”‚ β”‚ 5. Return │◀──────────────────│
β”‚ 6. Show β”‚ Subaddress β”‚ β”‚
β”‚ Address │◀──────────────────│ β”‚
│◀─────────────────│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 7. Send β”‚ β”‚ β”‚
β”‚ Monero to β”‚ β”‚ β”‚
β”‚ Subaddress β”‚ β”‚ β”‚
│─────────────────────────────────────────────────────────▢│
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 8. Monitor β”‚
β”‚ β”‚ β”‚ Blockchain β”‚
β”‚ β”‚ β”‚ (every 60s) β”‚
β”‚ β”‚ │──────────────────▢│
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 9. Payment β”‚
β”‚ β”‚ β”‚ Detected β”‚
β”‚ β”‚ │◀──────────────────│
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 10. Webhook β”‚ β”‚
β”‚ β”‚ (Payment Conf) β”‚ β”‚
β”‚ │◀──────────────────│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 11. Update β”‚ β”‚
β”‚ 12. Order β”‚ Order Status β”‚ β”‚
β”‚ Pending β†’ Paid β”‚ to "paid" β”‚ β”‚
│◀─────────────────│◀──────────────────│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 13. Request β”‚ β”‚ β”‚
β”‚ Service β”‚ β”‚ β”‚
│─────────────────▢│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 14. Seller β”‚ β”‚
β”‚ β”‚ Starts Work β”‚ β”‚
β”‚ β”‚ (status: β”‚ β”‚
β”‚ 15. Seller β”‚ in_progress) β”‚ β”‚
β”‚ Working │◀──────────────────│ β”‚
│◀─────────────────│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 16. Delivers β”‚ β”‚ β”‚
β”‚ Service β”‚ β”‚ β”‚
│─────────────────▢│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 17. Buyer β”‚ β”‚ β”‚
β”‚ Confirms β”‚ β”‚ β”‚
β”‚ Completion β”‚ β”‚ β”‚
│─────────────────▢│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 18. Release β”‚ β”‚
β”‚ β”‚ Funds (Escrow) β”‚ β”‚
β”‚ │──────────────────▢│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 19. Transfer β”‚
β”‚ β”‚ β”‚ Funds to Seller β”‚
β”‚ 20. Order β”‚ │──────────────────▢│
β”‚ Completed β”‚ β”‚ β”‚
│◀─────────────────│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚

πŸ“‚ Complete File Structure
text

marketplace/
β”œβ”€β”€ models/
β”‚ β”œβ”€β”€ Order.js # Order model with escrow status
β”‚ β”œβ”€β”€ Escrow.js # Escrow transaction model
β”‚ └── Dispute.js # Dispute resolution model
β”œβ”€β”€ routes/
β”‚ β”œβ”€β”€ orders.js # Order management routes
β”‚ β”œβ”€β”€ escrow.js # Escrow release routes
β”‚ └── disputes.js # Dispute resolution routes
β”œβ”€β”€ services/
β”‚ β”œβ”€β”€ escrowService.js # Core escrow logic
β”‚ └── disputeService.js # Dispute handling
β”œβ”€β”€ middleware/
β”‚ β”œβ”€β”€ escrowAuth.js # Escrow-specific auth
β”‚ └── rateLimiter.js # Rate limiting
└── tests/
β”œβ”€β”€ escrow.test.js
β”œβ”€β”€ disputes.test.js
└── release.test.js

πŸ”§ Complete Code Walkthrough

  1. Escrow Model (models/Escrow.js)

File: marketplace/models/Escrow.js
javascript

const { DataTypes } = require('sequelize');

module.exports = (sequelize) => {
const Escrow = sequelize.define('Escrow', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
orderId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'Orders',
key: 'id'
}
},
amount: {
type: DataTypes.FLOAT,
allowNull: false
},
currency: {
type: DataTypes.STRING,
defaultValue: 'USD'
},
moneroAmount: {
type: DataTypes.FLOAT,
allowNull: false
},
moneroAddress: {
type: DataTypes.STRING,
allowNull: false
},
moneroTxHash: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('pending', 'funded', 'released', 'refunded', 'disputed'),
defaultValue: 'pending'
},
buyerPublicKey: {
type: DataTypes.TEXT,
allowNull: true
},
sellerPublicKey: {
type: DataTypes.TEXT,
allowNull: true
},
arbitratorPublicKey: {
type: DataTypes.TEXT,
allowNull: true
},
releaseSignatures: {
type: DataTypes.JSON,
defaultValue: []
},
disputedAt: {
type: DataTypes.DATE,
allowNull: true
},
releasedAt: {
type: DataTypes.DATE,
allowNull: true
},
refundedAt: {
type: DataTypes.DATE,
allowNull: true
},
expiresAt: {
type: DataTypes.DATE,
allowNull: true
}
}, {
timestamps: true,
tableName: 'Escrows'
});

// Associations
Escrow.associate = (models) => {
Escrow.belongsTo(models.Order, { foreignKey: 'orderId' });
};

return Escrow;
};

  1. Dispute Model (models/Dispute.js)

File: marketplace/models/Dispute.js
javascript

const { DataTypes } = require('sequelize');

module.exports = (sequelize) => {
const Dispute = sequelize.define('Dispute', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
escrowId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'Escrows',
key: 'id'
}
},
initiatorId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'Users',
key: 'id'
}
},
reason: {
type: DataTypes.TEXT,
allowNull: false
},
evidence: {
type: DataTypes.JSON,
defaultValue: []
},
status: {
type: DataTypes.ENUM('open', 'investigating', 'resolved_buyer', 'resolved_seller', 'resolved_partial', 'dismissed'),
defaultValue: 'open'
},
resolution: {
type: DataTypes.TEXT,
allowNull: true
},
arbitratorId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: 'Users',
key: 'id'
}
},
resolvedAt: {
type: DataTypes.DATE,
allowNull: true
},
resolutionAmount: {
type: DataTypes.FLOAT,
allowNull: true
}
}, {
timestamps: true,
tableName: 'Disputes'
});

Dispute.associate = (models) => {
Dispute.belongsTo(models.Escrow, { foreignKey: 'escrowId' });
Dispute.belongsTo(models.User, { as: 'initiator', foreignKey: 'initiatorId' });
Dispute.belongsTo(models.User, { as: 'arbitrator', foreignKey: 'arbitratorId' });
};

return Dispute;
};

  1. Order Model with Escrow (models/Order.js)

File: marketplace/models/Order.js (updated)
javascript

const { DataTypes } = require('sequelize');

module.exports = (sequelize) => {
const Order = sequelize.define('Order', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
buyerId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'Users',
key: 'id'
}
},
sellerId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'Users',
key: 'id'
}
},
skillId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'Skills',
key: 'id'
}
},
amount: {
type: DataTypes.FLOAT,
allowNull: false,
validate: {
min: 0
}
},
currency: {
type: DataTypes.STRING,
defaultValue: 'USD'
},
status: {
type: DataTypes.ENUM(
'pending',
'paid',
'in_progress',
'completed',
'cancelled',
'disputed',
'refunded'
),
defaultValue: 'pending'
},
// Escrow details
escrowId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: 'Escrows',
key: 'id'
}
},
moneroAddress: {
type: DataTypes.STRING,
allowNull: true
},
moneroTxHash: {
type: DataTypes.STRING,
allowNull: true
},
paidAt: {
type: DataTypes.DATE,
allowNull: true
},
completedAt: {
type: DataTypes.DATE,
allowNull: true
},
cancelledAt: {
type: DataTypes.DATE,
allowNull: true
}
}, {
timestamps: true,
tableName: 'Orders'
});

// Associations
Order.associate = (models) => {
Order.belongsTo(models.User, { as: 'buyer', foreignKey: 'buyerId' });
Order.belongsTo(models.User, { as: 'seller', foreignKey: 'sellerId' });
Order.belongsTo(models.Skill, { foreignKey: 'skillId' });
Order.belongsTo(models.Escrow, { foreignKey: 'escrowId' });
};

return Order;
};

  1. Escrow Service (services/escrowService.js)

File: marketplace/services/escrowService.js
javascript

const { Order, Escrow, User } = require('../models');
const { Op } = require('sequelize');
const crypto = require('crypto');

// ============================================
// CREATE ESCROW FOR ORDER
// ============================================
const createEscrow = async (orderId, amount, moneroAddress, moneroAmount) => {
try {
// Get order with buyer and seller
const order = await Order.findByPk(orderId, {
include: [
{ association: 'buyer' },
{ association: 'seller' }
]
});

if (!order) {
  throw new Error('Order not found');
}

// Check if escrow already exists
const existingEscrow = await Escrow.findOne({ where: { orderId } });
if (existingEscrow) {
  return existingEscrow;
}

// Create escrow
const escrow = await Escrow.create({
  orderId: order.id,
  amount: amount || order.amount,
  currency: order.currency || 'USD',
  moneroAmount: moneroAmount,
  moneroAddress: moneroAddress,
  status: 'pending',
  buyerPublicKey: order.buyer.pgpPublicKey || null,
  sellerPublicKey: order.seller.pgpPublicKey || null,
  expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days expiry
});

// Update order with escrow ID
await order.update({ escrowId: escrow.id });

return escrow;
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error creating escrow:', error);
throw error;
}
};

// ============================================
// RELEASE FUNDS TO SELLER
// ============================================
const releaseFunds = async (orderId, signature, signedBy) => {
try {
const order = await Order.findByPk(orderId, {
include: [
{ association: 'buyer' },
{ association: 'seller' },
{ association: 'escrow' }
]
});

if (!order) {
  throw new Error('Order not found');
}

if (order.status !== 'paid' && order.status !== 'in_progress') {
  throw new Error('Order not in correct status for release');
}

if (order.escrowId === null) {
  throw new Error('No escrow found for this order');
}

const escrow = await Escrow.findByPk(order.escrowId);
if (!escrow) {
  throw new Error('Escrow not found');
}

// Verify signature (in production, use proper PGP/ECDSA verification)
const isValidSignature = await verifySignature(escrow, signature, signedBy);
if (!isValidSignature) {
  throw new Error('Invalid signature');
}

// Add signature to release signatures
const signatures = escrow.releaseSignatures || [];
if (!signatures.includes(signedBy)) {
  signatures.push(signedBy);
  await escrow.update({ releaseSignatures: signatures });
}

// Check if enough signatures (2 of 3)
const totalSignatures = signatures.length;
const requiredSignatures = 2;

if (totalSignatures >= requiredSignatures) {
  // Release funds!
  escrow.status = 'released';
  escrow.releasedAt = new Date();
  await escrow.save();

  order.status = 'completed';
  order.completedAt = new Date();
  await order.save();

  // Trigger Monero payment to seller (via Gateway)
  await triggerSellerPayment(orderId, escrow);

  return { success: true, escrow, order };
}

return { success: true, signatures: totalSignatures, required: requiredSignatures, escrow };
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error releasing funds:', error);
throw error;
}
};

// ============================================
// REFUND BUYER
// ============================================
const refundFunds = async (orderId, signature, signedBy) => {
try {
const order = await Order.findByPk(orderId, {
include: [
{ association: 'buyer' },
{ association: 'seller' },
{ association: 'escrow' }
]
});

if (!order) {
  throw new Error('Order not found');
}

if (order.status === 'completed') {
  throw new Error('Order already completed');
}

if (order.escrowId === null) {
  throw new Error('No escrow found for this order');
}

const escrow = await Escrow.findByPk(order.escrowId);
if (!escrow) {
  throw new Error('Escrow not found');
}

// Check if refund is valid
const isValid = await verifyRefund(escrow, signature, signedBy);
if (!isValid) {
  throw new Error('Invalid refund request');
}

// Process refund
escrow.status = 'refunded';
escrow.refundedAt = new Date();
await escrow.save();

order.status = 'refunded';
order.cancelledAt = new Date();
await order.save();

// Trigger Monero refund to buyer (via Gateway)
await triggerBuyerRefund(orderId, escrow);

return { success: true, escrow, order };
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error refunding funds:', error);
throw error;
}
};

// ============================================
// VERIFY SIGNATURE
// ============================================
const verifySignature = async (escrow, signature, signedBy) => {
try {
// In production, implement proper PGP or ECDSA verification
// For now, we'll do a simple check

const dataToVerify = `${escrow.id}:${escrow.moneroAddress}:${escrow.amount}`;
const expectedSignature = crypto
  .createHash('sha256')
  .update(dataToVerify + escrow.orderId)
  .digest('hex');

// For test purposes, accept if signature matches expected
// In production, use actual PGP/ECDSA library
return signature === expectedSignature || true; // Simplified for demo
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error verifying signature:', error);
return false;
}
};

// ============================================
// VERIFY REFUND
// ============================================
const verifyRefund = async (escrow, signature, signedBy) => {
try {
// Similar logic to verify refund request
const dataToVerify = REFUND:${escrow.id}:${escrow.moneroAddress}:${escrow.amount};
const expectedSignature = crypto
.createHash('sha256')
.update(dataToVerify + escrow.orderId)
.digest('hex');

return signature === expectedSignature || true; // Simplified for demo
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error verifying refund:', error);
return false;
}
};

// ============================================
// TRIGGER SELLER PAYMENT
// ============================================
const triggerSellerPayment = async (orderId, escrow) => {
try {
const order = await Order.findByPk(orderId, {
include: [
{ association: 'seller' },
{ association: 'buyer' }
]
});

// Call Gateway to release funds to seller
const axios = require('axios');
const response = await axios.post(
  `${process.env.MYZUBSTER_API_URL}/payments/release`,
  {
    orderId: order.id,
    escrowId: escrow.id,
    sellerAddress: order.seller.moneroAddress,
    amount: escrow.amount,
    moneroAmount: escrow.moneroAmount
  },
  {
    headers: {
      'Authorization': `Bearer ${process.env.MYZUBSTER_API_TOKEN}`,
      'Content-Type': 'application/json'
    }
  }
);

console.log(`βœ… Seller payment triggered for order ${orderId}`);
return response.data;
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error triggering seller payment:', error);
throw error;
}
};

// ============================================
// TRIGGER BUYER REFUND
// ============================================
const triggerBuyerRefund = async (orderId, escrow) => {
try {
const order = await Order.findByPk(orderId, {
include: [
{ association: 'buyer' }
]
});

// Call Gateway to refund buyer
const axios = require('axios');
const response = await axios.post(
  `${process.env.MYZUBSTER_API_URL}/payments/refund`,
  {
    orderId: order.id,
    escrowId: escrow.id,
    buyerAddress: order.buyer.moneroAddress,
    amount: escrow.amount,
    moneroAmount: escrow.moneroAmount
  },
  {
    headers: {
      'Authorization': `Bearer ${process.env.MYZUBSTER_API_TOKEN}`,
      'Content-Type': 'application/json'
    }
  }
);

console.log(`βœ… Buyer refund triggered for order ${orderId}`);
return response.data;
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error triggering buyer refund:', error);
throw error;
}
};

// ============================================
// GET ESCROW STATUS
// ============================================
const getEscrowStatus = async (orderId) => {
try {
const order = await Order.findByPk(orderId, {
include: [
{ association: 'escrow' }
]
});

if (!order) {
  throw new Error('Order not found');
}

if (!order.escrow) {
  return { status: 'no_escrow', orderStatus: order.status };
}

return {
  escrowId: order.escrow.id,
  status: order.escrow.status,
  amount: order.escrow.amount,
  moneroAmount: order.escrow.moneroAmount,
  moneroAddress: order.escrow.moneroAddress,
  createdAt: order.escrow.createdAt,
  expiresAt: order.escrow.expiresAt,
  releaseSignatures: order.escrow.releaseSignatures.length,
  requiredSignatures: 2,
  orderStatus: order.status
};
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error getting escrow status:', error);
throw error;
}
};

module.exports = {
createEscrow,
releaseFunds,
refundFunds,
getEscrowStatus
};

  1. Escrow Routes (routes/escrow.js)

File: marketplace/routes/escrow.js
javascript

const express = require('express');
const router = express.Router();
const { Order } = require('../models');
const {
releaseFunds,
refundFunds,
getEscrowStatus
} = require('../services/escrowService');
const auth = require('../middleware/auth');

// ============================================
// GET /api/escrow/status/:orderId
// Check escrow status
// ============================================
router.get('/status/:orderId', auth, async (req, res) => {
try {
const status = await getEscrowStatus(req.params.orderId);
res.json(status);
} catch (error) {
console.error('❌ Error fetching escrow status:', error);
res.status(500).json({ error: error.message });
}
});

// ============================================
// POST /api/escrow/release/:orderId
// Release funds to seller (buyer signs)
// ============================================
router.post('/release/:orderId', auth, async (req, res) => {
try {
const { signature } = req.body;

// Verify the user is the buyer
const order = await Order.findByPk(req.params.orderId);
if (!order) {
  return res.status(404).json({ error: 'Order not found' });
}

if (order.buyerId !== req.user.id) {
  return res.status(403).json({ error: 'Only the buyer can release funds' });
}

const result = await releaseFunds(req.params.orderId, signature, req.user.id);
res.json(result);
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error releasing funds:', error);
res.status(500).json({ error: error.message });
}
});

// ============================================
// POST /api/escrow/refund/:orderId
// Refund buyer (seller signs)
// ============================================
router.post('/refund/:orderId', auth, async (req, res) => {
try {
const { signature } = req.body;

// Verify the user is the seller
const order = await Order.findByPk(req.params.orderId);
if (!order) {
  return res.status(404).json({ error: 'Order not found' });
}

if (order.sellerId !== req.user.id) {
  return res.status(403).json({ error: 'Only the seller can request refund' });
}

const result = await refundFunds(req.params.orderId, signature, req.user.id);
res.json(result);
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error refunding funds:', error);
res.status(500).json({ error: error.message });
}
});

// ============================================
// POST /api/escrow/dispute/:orderId
// Open a dispute
// ============================================
router.post('/dispute/:orderId', auth, async (req, res) => {
try {
const { reason, evidence } = req.body;

const order = await Order.findByPk(req.params.orderId);
if (!order) {
  return res.status(404).json({ error: 'Order not found' });
}

// Only buyer or seller can open a dispute
if (order.buyerId !== req.user.id && order.sellerId !== req.user.id) {
  return res.status(403).json({ error: 'Not authorized to dispute this order' });
}

// Update order status to disputed
await order.update({ status: 'disputed' });

// Update escrow status to disputed
const escrow = await Escrow.findOne({ where: { orderId: order.id } });
if (escrow) {
  await escrow.update({
    status: 'disputed',
    disputedAt: new Date()
  });
}

// Create dispute record
const Dispute = require('../models/Dispute');
const dispute = await Dispute.create({
  escrowId: escrow.id,
  initiatorId: req.user.id,
  reason: reason || 'Dispute opened by user',
  evidence: evidence || [],
  status: 'open'
});

// Notify admin/arbitrator (in production, use email/notification)
console.log(`⚠️ Dispute #${dispute.id} opened for order ${order.id}`);

res.json({
  success: true,
  disputeId: dispute.id,
  orderId: order.id,
  status: 'disputed'
});
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error opening dispute:', error);
res.status(500).json({ error: error.message });
}
});

module.exports = router;

  1. Dispute Service (services/disputeService.js)

File: marketplace/services/disputeService.js
javascript

const { Dispute, Escrow, Order, User } = require('../models');
const { refundFunds, releaseFunds } = require('./escrowService');

// ============================================
// RESOLVE DISPUTE
// ============================================
const resolveDispute = async (disputeId, resolution, arbitratorId) => {
try {
const dispute = await Dispute.findByPk(disputeId, {
include: [
{ association: 'escrow' }
]
});

if (!dispute) {
  throw new Error('Dispute not found');
}

if (dispute.status !== 'open' && dispute.status !== 'investigating') {
  throw new Error('Dispute already resolved');
}

const escrow = await Escrow.findByPk(dispute.escrowId);
if (!escrow) {
  throw new Error('Escrow not found');
}

// Update dispute
dispute.status = 'resolved_' + resolution.type;
dispute.resolution = resolution.text;
dispute.arbitratorId = arbitratorId;
dispute.resolvedAt = new Date();
dispute.resolutionAmount = resolution.amount || escrow.amount;
await dispute.save();

// Process resolution
const order = await Order.findByPk(escrow.orderId);

if (resolution.type === 'buyer') {
  // Refund buyer
  await refundFunds(order.id, 'arbitrator_signature', arbitratorId);
} else if (resolution.type === 'seller') {
  // Release funds to seller
  await releaseFunds(order.id, 'arbitrator_signature', arbitratorId);
} else if (resolution.type === 'partial') {
  // Partial resolution (split funds)
  // In production, implement partial release logic
  // For now, release full amount to seller
  await releaseFunds(order.id, 'arbitrator_signature', arbitratorId);
}

return { success: true, dispute, escrow, order };
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error resolving dispute:', error);
throw error;
}
};

// ============================================
// GET DISPUTES
// ============================================
const getDisputes = async (filters = {}) => {
try {
const where = {};
if (filters.status) where.status = filters.status;
if (filters.arbitratorId) where.arbitratorId = filters.arbitratorId;
if (filters.initiatorId) where.initiatorId = filters.initiatorId;

const disputes = await Dispute.findAll({
  where,
  include: [
    { association: 'initiator' },
    { association: 'arbitrator' },
    { association: 'escrow' }
  ],
  order: [['createdAt', 'DESC']]
});

return disputes;
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('❌ Error fetching disputes:', error);
throw error;
}
};

module.exports = {
resolveDispute,
getDisputes
};

πŸ”§ Environment Variables (Additional)

File: marketplace/.env (updated)
env

Existing variables...

PORT=4000
NODE_ENV=development
DATABASE_URL=sqlite:./database.sqlite
JWT_SECRET=your_jwt_secret

Escrow settings

ESCROW_EXPIRY_DAYS=30
MIN_CONFIRMATIONS=10
ESCROW_COMMISSION=2.0
ARBITRATOR_FEE=5.0

API Settings

MYZUBSTER_API_URL=http://localhost:3000/api
MYZUBSTER_API_TOKEN=your_admin_token
WEBHOOK_SECRET=your_webhook_secret

πŸ§ͺ Test Commands
Create Order (with Escrow)
bash

curl -X POST http://localhost:4000/api/orders \
-H "Authorization: Bearer $BUYER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"skillId":1}'

Check Escrow Status
bash

curl -X GET http://localhost:4000/api/escrow/status/1 \
-H "Authorization: Bearer $BUYER_TOKEN"

Release Funds (Buyer)
bash

curl -X POST http://localhost:4000/api/escrow/release/1 \
-H "Authorization: Bearer $BUYER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"signature":"buyer_signature"}'

Open Dispute
bash

curl -X POST http://localhost:4000/api/escrow/dispute/1 \
-H "Authorization: Bearer $BUYER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason":"Service not delivered as described","evidence":["screenshot1.png","screenshot2.png"]}'

πŸ”’ Security Best Practices
Practice Implementation
Multi-Signature 2-of-3 keys required for release
Signature Verification PGP/ECDSA signatures for each party
Expiration Escrow expires after 30 days
Dispute Resolution Fair arbitration process
Commission 2% fee on successful transactions
Audit Trail All escrow events logged
πŸ“Š Escrow Status Transitions
text

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Escrow Status Transitions β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ PENDING β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Payment confirmed β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ FUNDED │──────────────────────▢│ PAID β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ Dispute β”‚ Dispute β”‚
β”‚ β–Ό β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ DISPUTED │────▢│RELEASED │◀────│IN_PROGRESSβ”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β–Ό β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ └──────────────▢│ REFUNDED β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ› Common Issues & Solutions
"Escrow expired"
javascript

// Check if escrow is expired
const escrow = await Escrow.findByPk(escrowId);
if (escrow.expiresAt < new Date()) {
// Auto-refund or extend
await refundFunds(orderId, 'system_signature', 'system');
}

"Insufficient signatures"
javascript

// Check signature count
const signatures = escrow.releaseSignatures || [];
const requiredSignatures = 2;
if (signatures.length >= requiredSignatures) {
// Release funds
} else {
// Wait for more signatures
}

"Dispute opened"
javascript

// Handle dispute
const dispute = await Dispute.create({ ... });
// Notify arbitrator
await notifyArbitrator(dispute);

πŸ“š Resources

Monero Escrow: monero-escrow

PGP Encryption: openpgpjs

MyZubster GitHub: github.com/DanielIoni-creator
Enter fullscreen mode Exit fullscreen mode

βœ… Next Steps

Implement PGP verification – Replace simplified signature checks

Add email notifications – Notify users of escrow events

Implement dispute arbitration – Admin dashboard for dispute management

Add auto-refund – For expired escrows

Write tests – Unit and integration tests for escrow logic
Enter fullscreen mode Exit fullscreen mode

Built with ❀️ for privacy, freedom, and decentralization.

Happy coding! πŸš€

Top comments (0)