Escrows Guide
Escrows provide a trust layer for high-value or conditional transactions between AI agents and merchants. Funds are locked in a secure on-chain account and only released when predefined conditions are met. This protects both parties: merchants are guaranteed payment upon delivery, and vault owners can reclaim funds if conditions are not met.
Lifecycle#
An escrow progresses through a defined set of states:
+--> released
|
funded -----+-----+--> refunded
|
+--> disputed --+--> refunded
| Status | Description |
|---|---|
| funded | Escrow is active. Funds are locked on-chain. |
| released | Funds have been sent to the merchant. Terminal state. |
| refunded | Funds have been returned to the vault. Terminal state. |
| disputed | A dispute has been raised. Can transition to refunded but NOT to released. |
State Transitions#
| From | To | Action |
|---|---|---|
| funded | released | Release (conditions met) |
| funded | refunded | Refund (conditions not met, or cancellation) |
| funded | disputed | Dispute raised |
| disputed | refunded | Refund after dispute resolution |
Once an escrow is released or refunded, it cannot be changed. These are terminal states.
Condition Types#
When creating an escrow, you specify a condition_type that determines when funds can be released.
time_release#
Funds are released after a specified time period.
{
"condition_type": "time_release",
"condition_data": {
"release_after_hours": 24
},
"expires_in_hours": 48
}
Use case: Subscription trials, time-delayed purchases.
oracle_confirmation#
An external oracle (off-chain data source) confirms a real-world event, triggering release.
{
"condition_type": "oracle_confirmation",
"condition_data": {
"oracle_id": "oracle_weather_123",
"expected_value": "delivered"
},
"expires_in_hours": 72
}
Use case: Insurance payouts, event-based settlements.
multi_sig_approval#
Multiple parties must sign to release the funds.
{
"condition_type": "multi_sig_approval",
"condition_data": {
"required_signatures": 2,
"signers": ["signer_1_pubkey", "signer_2_pubkey", "signer_3_pubkey"]
},
"expires_in_hours": 168
}
Use case: High-value B2B transactions, committee-approved spending.
delivery_confirmation#
Funds are released when delivery is confirmed by the buyer or a tracking system.
{
"condition_type": "delivery_confirmation",
"condition_data": {
"tracking_number": "1Z999AA10123456784",
"carrier": "ups"
},
"expires_in_hours": 120
}
Use case: E-commerce purchases, physical goods delivery.
Integration Patterns#
Pattern 1: Automatic Escrow via Policy#
Configure the policy engine to automatically route high-value payments through escrow:
{
"escrow_rules": {
"require_escrow_above": 200,
"default_escrow_expiry_hours": 48
}
}
Any payment above 200 USDC will automatically create an escrow instead of settling directly.
Pattern 2: Agent-Initiated Escrow#
The agent explicitly creates an escrow for a specific transaction:
TypeScript
Pattern 3: Webhook-Driven Release#
Use webhooks to automate the escrow lifecycle:
app.post("/webhooks/subcent", (req, res) => {
const event = JSON.parse(req.body);
switch (event.event) {
case "escrow.funded":
// Notify the merchant that funds are secured
notifyMerchant(event.data.merchant_id, event.data.escrow_id);
break;
case "escrow.released":
// Update your order system
markOrderComplete(event.data.escrow_id);
break;
case "escrow.disputed":
// Alert your support team
createSupportTicket(event.data.escrow_id, event.data.reason);
break;
}
res.status(200).json({ received: true });
});
Dispute Resolution Flow#
When a dispute is raised on an escrow, it enters the disputed state. The dispute resolution process is:
1. Raise the Dispute#
Either the vault owner or the agent can raise a dispute via the API:
const result = await fetch(`https://api.subcent.io/v1/escrows/${escrowId}/dispute`, {
method: "POST",
headers: {
"Authorization": "Bearer sc_test_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ reason: "Product not as described" }),
});
2. Review#
The escrow.disputed webhook is sent to all registered endpoints. Both parties can provide evidence through your application.
3. Resolution#
A disputed escrow can only be refunded (funds returned to vault). It cannot be released to the merchant.
// After dispute resolution, refund the escrow
const result = await fetch(`https://api.subcent.io/v1/escrows/${escrowId}/refund`, {
method: "POST",
headers: { "Authorization": "Bearer sc_test_your_key" },
});
In the current version, dispute resolution is handled off-chain through your application logic. On-chain arbitration will be available in a future release.
Expiration#
Every escrow has an expiration time set at creation (via expires_in_hours, default 24 hours). When an escrow expires:
- Release is blocked. Attempting to release an expired escrow returns an
escrow_expirederror. - Refund is still allowed. The vault owner can reclaim the funds.
- No automatic action is taken on expiration -- the vault owner must explicitly refund.
Checking Expiration#
const escrow = await fetch(`https://api.subcent.io/v1/escrows/${escrowId}`, {
headers: { "Authorization": "Bearer sc_test_your_key" },
}).then(r => r.json());
const expiresAt = new Date(escrow.expires_at);
const isExpired = new Date() > expiresAt;
if (isExpired && escrow.status === "funded") {
// Refund the expired escrow
await fetch(`https://api.subcent.io/v1/escrows/${escrowId}/refund`, {
method: "POST",
headers: { "Authorization": "Bearer sc_test_your_key" },
});
}