Export as

Escrow Guide

How blockchain-based escrow works in OFFER-HUB — fund locking, release flows, and dispute handling.

Escrow is the core security mechanism in OFFER-HUB. When a buyer pays for a service, funds are locked in a Stellar smart contract until the work is delivered and approved. Neither party can unilaterally access the funds — the contract enforces fair resolution.

Tip

Make sure you understand the Order Lifecycle before diving into escrow mechanics.

How Escrow Works

OFFER-HUB uses Trustless Work smart contracts on the Stellar blockchain for non-custodial escrow. This means:

  1. Funds are on-chain — Not in OFFER-HUB's database
  2. Contract enforces rules — Release requires proper signatures
  3. Transparent — Anyone can verify escrow state on Stellar
  4. Non-custodial — OFFER-HUB cannot access funds without proper authorization

Escrow Participants

RoleDescription
BuyerDeposits funds into escrow
SellerReceives funds when work is approved
PlatformOFFER-HUB's signer for dispute resolution
ArbiterTrustless Work's contract for on-chain enforcement

Escrow Lifecycle

State Machine

Mermaid
Rendering diagram…

State Descriptions

StateDescription
ESCROW_CREATINGSmart contract being deployed
ESCROW_CREATEDContract ready, waiting for funding
ESCROW_FUNDINGUSDC transfer to contract in progress
ESCROW_FUNDEDFunds locked, work can begin
IN_PROGRESSSeller is working on the order
RELEASINGRelease transaction in progress
DISPUTINGDispute transaction in progress
DISPUTEDAwaiting resolution
CLOSEDFunds released to seller
REFUNDEDFunds returned to buyer

Creating Escrow

After reserving funds, create the escrow contract:

bash
curl -X POST http://localhost:4000/api/v1/orders/ord_xyz789/escrow \
  -H "Authorization: Bearer ohk_live_your_api_key" \
  -H "Content-Type: application/json"

Response:

json
{
  "data": {
    "id": "ord_xyz789",
    "status": "ESCROW_CREATING",
    "escrow": {
      "contractId": "CDLZFC...",
      "status": "creating"
    }
  }
}

The contract deployment happens asynchronously. Listen for events or poll the order status.

Funding Escrow

Once the contract is created, fund it with USDC:

bash
curl -X POST http://localhost:4000/api/v1/orders/ord_xyz789/escrow/fund \
  -H "Authorization: Bearer ohk_live_your_api_key"

This triggers an on-chain USDC transfer from the buyer's invisible wallet to the escrow contract.

What Happens During Funding

  1. Debit buyer's balance — Reserved funds become locked
  2. Sign transaction — Platform signs with buyer's encrypted key
  3. Submit to Stellar — USDC sent to contract address
  4. Confirm on-chain — Wait for ledger confirmation
  5. Update status — Order becomes IN_PROGRESS
Note

Funding typically takes 5-10 seconds. The order status progresses through ESCROW_FUNDINGESCROW_FUNDEDIN_PROGRESS.

Releasing Funds

When the buyer approves the work, release funds to the seller:

bash
curl -X POST http://localhost:4000/api/v1/orders/ord_xyz789/resolution/release \
  -H "Authorization: Bearer ohk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"requestedBy": "usr_buyer123"}'

The 3-Transaction Release Flow

Releasing funds requires three separate blockchain transactions:

StepTransactionSigner
1complete_escrowBuyer approves work
2release_escrowPlatform authorizes release
3Claim fundsSeller receives USDC
Mermaid
Rendering diagram…

Release Response

json
{
  "data": {
    "id": "ord_xyz789",
    "status": "CLOSED",
    "resolution": {
      "type": "released",
      "amount": "50.00",
      "recipient": "usr_seller456",
      "completedAt": "2026-02-25T14:00:00.000Z"
    }
  }
}

Refunding (Dispute Flow)

If there's a problem, the buyer can request a refund through the dispute process:

Step 1: Open Dispute

bash
curl -X POST http://localhost:4000/api/v1/orders/ord_xyz789/resolution/dispute \
  -H "Authorization: Bearer ohk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "requestedBy": "usr_buyer123",
    "reason": "Work not delivered as specified"
  }'

Step 2: Platform Resolves

After review, the platform resolves the dispute:

bash
curl -X POST http://localhost:4000/api/v1/disputes/dsp_abc123/resolve \
  -H "Authorization: Bearer ohk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "resolution": "refund",
    "note": "Seller did not deliver agreed work"
  }'

The 2-Transaction Refund Flow

StepTransactionSigner
1dispute_escrowBuyer opens dispute
2resolve_disputePlatform refunds buyer
Warning

Disputes require platform intervention. The smart contract won't release funds until the platform signs the resolution transaction.

Using the SDK

The SDK simplifies escrow operations:

typescript
import { OfferHubSDK } from '@offerhub/sdk';

const sdk = new OfferHubSDK({
  apiUrl: 'http://localhost:4000',
  apiKey: 'ohk_live_your_api_key'
});

// Create and fund escrow in sequence
const order = await sdk.orders.get('ord_xyz789');

// Create escrow contract
await sdk.escrow.create(order.id);

// Wait for contract creation (or use events)
await sdk.orders.waitForStatus(order.id, 'ESCROW_CREATED');

// Fund the escrow
await sdk.escrow.fund(order.id);

// Wait for funding confirmation
await sdk.orders.waitForStatus(order.id, 'IN_PROGRESS');

// Later: release to seller
await sdk.resolution.release(order.id, {
  requestedBy: order.buyerId
});

Listening to Escrow Events

typescript
const events = sdk.events.subscribe();

events.on('order.escrow_created', (data) => {
  console.log('Contract deployed:', data.contractId);
});

events.on('order.escrow_funded', (data) => {
  console.log('Funds locked:', data.amount);
});

events.on('order.released', (data) => {
  console.log('Funds released to seller');
});

events.on('order.disputed', (data) => {
  console.log('Dispute opened:', data.reason);
});

On-Chain vs Off-Chain Balance

OFFER-HUB tracks two types of balances:

Off-Chain Balance

Stored in the database for fast operations:

json
{
  "available": "100.00",
  "reserved": "50.00"
}
  • Available — Can be used for new orders or withdrawn
  • Reserved — Locked for pending orders (before escrow funding)

On-Chain Balance

Actual USDC in the user's Stellar wallet:

json
{
  "stellar_address": "GCV24WNJ...",
  "usdc_balance": "150.00"
}

Balance Flow During Escrow

StageAvailableReservedOn-Chain
Initial100.000.00100.00
After reserve50.0050.00100.00
After funding50.000.0050.00 (in escrow)
After release50.000.0050.00
Note

When escrow is funded, the reserved amount leaves both the off-chain balance AND the on-chain wallet. It's now in the smart contract.

Platform Wallet

The platform wallet is a special Stellar account that:

  1. Co-signs transactions — Required for release/refund
  2. Collects fees — Platform fees are sent here
  3. Never holds user funds — Only signs, doesn't custody

Configure it in your environment:

env
PLATFORM_USER_ID=usr_platform

Error Handling

Common Escrow Errors

ErrorCauseSolution
ESCROW_ALREADY_EXISTSDuplicate creation attemptCheck order status first
ESCROW_NOT_FUNDEDTrying to release unfunded escrowFund the escrow first
INVALID_STATE_TRANSITIONWrong order stateFollow the state machine
INSUFFICIENT_FUNDSWallet doesn't have enough USDCCheck balance before funding
CONTRACT_ERRORStellar contract failureCheck Trustless Work status

Handling Timeouts

Blockchain transactions can take time. Handle timeouts gracefully:

typescript
try {
  await sdk.escrow.fund(orderId);
} catch (error) {
  if (error.code === 'FUNDING_TIMEOUT') {
    // Check if transaction was actually submitted
    const order = await sdk.orders.get(orderId);
    if (order.status === 'ESCROW_FUNDING') {
      // Transaction pending, wait for webhook
      console.log('Funding in progress, waiting for confirmation...');
    }
  }
}

Webhooks for Escrow Events

Subscribe to escrow lifecycle events:

bash
curl -X POST http://localhost:4000/api/v1/webhooks \
  -H "Authorization: Bearer ohk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks",
    "events": [
      "escrow.created",
      "escrow.funded",
      "escrow.released",
      "escrow.disputed",
      "escrow.refunded"
    ],
    "secret": "your-webhook-secret"
  }'

See Webhooks for payload formats and signature verification.

Security Considerations

  1. Never expose private keys — All signing happens server-side
  2. Verify webhook signatures — Prevent spoofed events
  3. Use idempotency keys — Prevent duplicate funding
  4. Monitor contract state — Cross-check with Stellar explorer

Next Steps