Milestone Payments¶
Project-based payments released as deliverables are completed — for freelancers, dev shops, consultants, and any work that should be paid on completion, not on a clock.
Overview¶
Milestone policies define up to 4 payment stages, each with its own amount and due timestamp. Payments release when conditions are met (time-based, manual approval, or automatic). The total escrow amount is delegated upfront, but funds stay in the user's wallet until each milestone executes.
User creates policy --> Delegates total escrow amount
|
+----------+-----------+-----------+
| | | |
Milestone 1 Milestone 2 Milestone 3 Milestone 4
$50 (Week 1) $100 (Week 2) $100 (Week 3) $50 (Week 4)
Design Frontend Backend Deploy
When to Use¶
| Good For | Not Ideal For |
|---|---|
| Freelance projects with deliverables | Predictable recurring billing |
| Software development (phased delivery) | High-frequency micro-payments |
| Consulting engagements (deliverable-based) | Ongoing services with fixed cost |
| Content creation (episode/release-based) | Variable usage patterns |
| Any project where payment follows completion | Simple monthly subscriptions |
| Escrow-style agreements |
On-Chain Specification¶
PolicyType::Milestone {
milestone_amounts: [u64; 4], // Amount for each milestone (lamports)
milestone_timestamps: [i64; 4], // Absolute timestamps for each milestone
current_milestone: u8, // Next milestone to process (0-3)
release_condition: u8, // Bitmap: release requirements
total_milestones: u8, // How many milestones configured (1-4)
escrow_amount: u64, // Total amount escrowed (sum of all milestones)
padding: [u8; 53], // 128-byte alignment
}
Release Condition Bitmap¶
The release_condition field is an 8-bit bitmap where each bit controls a requirement:
| Bit | Value | Constant | Description |
|---|---|---|---|
| 0 | 0b0001 (1) |
RELEASE_DUE_DATE |
Timestamp must be reached |
| 1 | 0b0010 (2) |
RELEASE_GATEWAY |
Gateway authority must sign |
| 2 | 0b0100 (4) |
RELEASE_OWNER |
Policy owner (payer) must sign |
| 3 | 0b1000 (8) |
RELEASE_RECIPIENT |
Recipient must sign |
Important: Bits 1-3 are mutually exclusive — at most one signer requirement can be set. A value of 0 means no restrictions (anyone can trigger anytime).
Common Release Conditions¶
| Value | Binary | Due Date? | Signer Required | Behavior |
|---|---|---|---|---|
| 0 | 0b0000 |
No | None | Anyone can trigger anytime |
| 1 | 0b0001 |
Yes | None | Anyone can trigger after due date |
| 2 | 0b0010 |
No | Gateway | Gateway must sign |
| 3 | 0b0011 |
Yes | Gateway | Gateway signs + due date passed |
| 4 | 0b0100 |
No | Owner | Payer must approve |
| 5 | 0b0101 |
Yes | Owner | Payer approves + due date passed |
| 8 | 0b1000 |
No | Recipient | Recipient must claim |
| 9 | 0b1001 |
Yes | Recipient | Recipient claims after due date |
Account Size¶
Each Milestone variant is exactly 128 bytes, consistent with all other policy types.
Creating a Milestone Payment¶
Basic Example — Freelance Project¶
import { Tributary } from "@tributary-so/sdk";
import { BN } from "@coral-xyz/anchor";
import { createMemoBuffer } from "@tributary-so/sdk";
import { PublicKey, Transaction } from "@solana/web3.js";
const sdk = new Tributary(connection, wallet);
const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const recipient = new PublicKey("BxKpT3mZQ5HgeRZFMfWVBpDCmCN8eYwGmCjL7m9mVq");
const gateway = new PublicKey("6ntm5rWqDFefET8RFyZV73FcdqxPMbc7Tso3pCMWk4w4");
const now = Math.floor(Date.now() / 1000);
const milestoneAmounts = [
new BN(50_000_000), // $50 - Design & planning
new BN(100_000_000), // $100 - Frontend development
new BN(100_000_000), // $100 - Backend integration
new BN(50_000_000), // $50 - Testing & deployment
];
const milestoneTimestamps = [
new BN(now + 86400 * 7), // Week 1: Design done
new BN(now + 86400 * 14), // Week 2: Frontend done
new BN(now + 86400 * 21), // Week 3: Backend done
new BN(now + 86400 * 28), // Week 4: Deploy done
];
const instructions = await sdk.createMilestone(
USDC_MINT,
recipient,
gateway,
milestoneAmounts,
milestoneTimestamps,
1, // releaseCondition: time-based only (0b0001)
createMemoBuffer("project_abc", 64)
);
const tx = new Transaction().add(...instructions);
const signature = await sendAndConfirm(connection, tx, [wallet.payer]);
Manual Approval Required¶
// Gateway must sign off on each milestone (quality gate)
const instructions = await sdk.createMilestone(
USDC_MINT,
recipient,
gateway,
milestoneAmounts,
milestoneTimestamps,
3, // 0b0011: due date + gateway signer required
createMemoBuffer("consulting_q1", 64)
);
Fewer than 4 Milestones¶
// Only 2 milestones — SDK pads the rest with zeros
const instructions = await sdk.createMilestone(
USDC_MINT,
recipient,
gateway,
[new BN(200_000_000), new BN(300_000_000)], // $200 + $300
[new BN(now + 86400 * 14), new BN(now + 86400 * 30)],
1,
createMemoBuffer("simple_project", 64)
);
How It Works¶
Payment Execution Flow¶
- Policy creation — user creates policy, delegates the total escrow amount (sum of all milestones)
- Waiting —
current_milestonetimestamp and/or signer condition not yet met - Execution — when conditions are met, anyone calls
execute_payment - Transfer — the current milestone amount moves to recipient, minus fees
- Advance —
current_milestoneincrements - Completion — after the last milestone, the policy auto-pauses
Release Condition Validation¶
The protocol checks the bitmap before executing:
- If bit 0 is set (
RELEASE_DUE_DATE): verifynow >= milestone_timestamps[current_milestone] - If bit 1 is set (
RELEASE_GATEWAY): verify gateway authority signed the transaction - If bit 2 is set (
RELEASE_OWNER): verify the policy owner (payer) signed - If bit 3 is set (
RELEASE_RECIPIENT): verify the recipient signed
If any required condition fails, the transaction reverts.
Escrow Calculation¶
const escrowAmount = milestoneAmounts.reduce(
(sum, amount) => sum.add(amount),
new BN(0)
);
// Total delegated = sum of all milestones
Managing Milestone Payments¶
Query Status¶
const policy = await sdk.getPaymentPolicy(policyPda);
const ms = policy.policyType.milestone;
console.log("Current milestone:", ms.currentMilestone);
console.log("Total milestones:", ms.totalMilestones);
console.log("Escrow amount:", ms.escrowAmount.toString());
console.log("Release condition:", ms.releaseCondition);
// Check if next milestone is due
const nextAmount = ms.milestoneAmounts[ms.currentMilestone];
const nextTime = ms.milestoneTimestamps[ms.currentMilestone];
const isDue = Date.now() / 1000 >= nextTime.toNumber();
console.log(
`Next: $${nextAmount.toString()} due ${new Date(nextTime.toNumber() * 1000)}`
);
Execute a Milestone¶
// For manual-approval milestones, the authorized signer submits:
const instructions = await sdk.executePayment(
policyPda,
recipient,
tokenMint,
gateway
);
const tx = new Transaction().add(...instructions);
await sendAndConfirm(connection, tx, [signer]);
Pause / Cancel¶
// Pause -- stops milestone execution
await sdk.changePaymentPolicyStatus(tokenMint, policyId, { paused: {} });
// Resume
await sdk.changePaymentPolicyStatus(tokenMint, policyId, { active: {} });
// Cancel -- deletes the policy, revokes delegation
await sdk.deletePaymentPolicy(tokenMint, policyId);
Use Case Examples¶
Consulting Engagement¶
const monthlyMilestones = [
new BN(500_000_000), // $500 - Month 1: Strategy
new BN(400_000_000), // $400 - Month 2: Implementation
new BN(300_000_000), // $300 - Month 3: Optimization
];
const monthlyTimestamps = [
new BN(now + 86400 * 30),
new BN(now + 86400 * 60),
new BN(now + 86400 * 90),
];
// Owner must approve each release (quality gate)
await sdk.createMilestone(
USDC_MINT,
recipient,
gateway,
monthlyMilestones,
monthlyTimestamps,
5, // 0b0101: due date + owner approval
createMemoBuffer("consulting_retainer", 64)
);
Content Series with Auto-Release¶
const episodePayments = [
new BN(10_000_000), // $10 per episode
new BN(10_000_000),
new BN(10_000_000),
new BN(10_000_000),
];
const episodeTimestamps = [
new BN(now + 86400 * 7),
new BN(now + 86400 * 14),
new BN(now + 86400 * 21),
new BN(now + 86400 * 28),
];
// Auto-release when due date passes
await sdk.createMilestone(
USDC_MINT,
recipient,
gateway,
episodePayments,
episodeTimestamps,
1, // 0b0001: time-based only
createMemoBuffer("podcast_series", 64)
);
Troubleshooting¶
| Error | Cause | Fix |
|---|---|---|
PaymentNotDue |
Milestone timestamp hasn't passed | Wait for the scheduled time |
InvalidSigner |
Required signer didn't sign the tx | Include the correct signer in the transaction |
InvalidAmount |
Not applicable for milestones | Milestone amounts are fixed at creation |
| Milestone not advancing | Previous milestone not yet paid | Execute current milestone first |
Comparison with Other Policy Types¶
| Subscription | Milestone | Pay-as-you-go | |
|---|---|---|---|
| Amount | Fixed per period | Variable per milestone | Variable per claim |
| Timing | Fixed schedule | Event/timestamp based | On-demand |
| Flexibility | Low | Medium | High |
| Predictability | High | Medium | Low |
| Best For | Recurring services | Project deliverables | Variable usage |
| User Control | Set up once | Approve per milestone | Period limits |