Milestone Payments
Milestone payments allow you to release funds incrementally as project phases are completed. This is ideal for long-term projects, freelance work, or any scenario where deliverables are completed in stages.
How It Works
- Create Escrow Order: Set
releaseType: "MILESTONE_LOCKED" - Define Milestones: Create multiple milestones with specific amounts
- Complete Milestones: Mark milestones as completed sequentially
- Automatic Release: Funds are released as each milestone is completed
Use Cases
- Freelance Projects: Release payment as features are delivered
- Construction: Pay for completed phases (foundation, framing, etc.)
- Software Development: Release funds for MVP, beta, and production
- Consulting: Pay for completed deliverables or time periods
- Content Creation: Pay for drafts, revisions, and final delivery
Creating Milestones
Example: 3-Milestone Project
// Milestone 0: Project kickoff (30%)
await createMilestone({
milestoneIndex: 0,
description: 'Project kickoff and planning - 30%',
amount: '300.00',
percentage: 30,
});
// Milestone 1: Mid-point delivery (50%)
await createMilestone({
milestoneIndex: 1,
description: 'Core features completed - 50%',
amount: '500.00',
percentage: 50,
});
// Milestone 2: Final delivery (20%)
await createMilestone({
milestoneIndex: 2,
description: 'Final delivery and documentation - 20%',
amount: '200.00',
percentage: 20,
});
Milestone Best Practices
- Sequential Indexing: Always use 0, 1, 2, ... in order
- Clear Descriptions: Make it obvious what completion means
- Reasonable Amounts: Don't create too many tiny milestones
- Total Check: Ensure milestone amounts don't exceed order total
Completing Milestones
Milestones must be completed in order:
// Step 1: Complete milestone 0
await completeMilestone(milestone0Id, {
completedBy: merchantAddress,
completionProof: proofHash,
});
// Step 2: Wait for release (automatic if autoReleaseOnProof is true)
// Check order status...
// Step 3: Complete milestone 1
await completeMilestone(milestone1Id, {
completedBy: merchantAddress,
completionProof: proofHash,
});
Completion Proof
Provide verifiable proof of milestone completion:
import { ethers } from 'ethers';
const completionData = {
milestoneIndex: 0,
completedAt: new Date().toISOString(),
deliverables: [
'Feature A implemented',
'Feature B tested',
'Documentation updated',
],
signedBy: merchantAddress,
};
const proofHash = ethers.keccak256(
ethers.toUtf8Bytes(JSON.stringify(completionData))
);
Automatic Release
If autoReleaseOnProof is enabled:
- Milestone marked as completed
- Funds automatically released to merchant
- Settlement scheduled for off-ramp
- Next milestone becomes available
Complete Example
// 1. Create payment intent with milestone release type
const intent = await createPaymentIntent({
amount: '1000.00',
currency: 'USDC',
type: 'DELIVERY_VS_PAYMENT',
settlementMethod: 'OFF_RAMP_MOCK',
settlementDestination: 'bank_account_123',
metadata: {
releaseType: 'MILESTONE_LOCKED',
autoRelease: true,
},
});
// 2. Create milestones
const milestones = [
{ milestoneIndex: 0, amount: '300.00', description: 'Kickoff - 30%' },
{ milestoneIndex: 1, amount: '500.00', description: 'Mid-point - 50%' },
{ milestoneIndex: 2, amount: '200.00', description: 'Final - 20%' },
];
for (const milestone of milestones) {
await createMilestone(intent.id, milestone);
}
// 3. As project progresses, complete milestones
await completeMilestone(milestone0Id, {
completedBy: merchantAddress,
completionProof: generateProof('Milestone 0 deliverables'),
});
// Funds are automatically released!
Status Tracking
Monitor milestone status:
const conditionalPayment = await getConditionalPayment(intentId);
const milestones = conditionalPayment.milestones;
milestones.forEach((milestone) => {
console.log(`Milestone ${milestone.milestoneIndex}: ${milestone.status}`);
// PENDING, COMPLETED, or RELEASED
});
Common Patterns
50/50 Split
[
{ milestoneIndex: 0, amount: '500.00', description: '50% upfront' },
{ milestoneIndex: 1, amount: '500.00', description: '50% on completion' },
]
30/40/30 Pattern
[
{ milestoneIndex: 0, amount: '300.00', description: '30% start' },
{ milestoneIndex: 1, amount: '400.00', description: '40% mid-point' },
{ milestoneIndex: 2, amount: '300.00', description: '30% final' },
]
Equal Quarters
[
{ milestoneIndex: 0, amount: '250.00', description: '25% - Phase 1' },
{ milestoneIndex: 1, amount: '250.00', description: '25% - Phase 2' },
{ milestoneIndex: 2, amount: '250.00', description: '25% - Phase 3' },
{ milestoneIndex: 3, amount: '250.00', description: '25% - Phase 4' },
]
Error Handling
Previous Milestone Not Completed
try {
await completeMilestone(milestone2Id, {...});
} catch (error) {
if (error.code === 'invalid_request') {
// Complete milestone 1 first
await completeMilestone(milestone1Id, {...});
}
}
Milestone Already Released
const milestone = await getMilestone(milestoneId);
if (milestone.status === 'RELEASED') {
console.log('Milestone already released, funds sent');
}