subcent

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" },
});
i

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_expired error.
  • 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" },
  });
}