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.
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:
— Not in OFFER-HUB's database
— Release requires proper signatures
— Anyone can verify escrow state on Stellar
— OFFER-HUB cannot access funds without proper authorization
Role Description Deposits funds into escrow Receives funds when work is approved OFFER-HUB's signer for dispute resolution Trustless Work's contract for on-chain enforcement
Escrow Lifecycle
State Description 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:
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:
{
"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:
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.
— Reserved funds become locked
— Platform signs with buyer's encrypted key
— USDC sent to contract address
— Wait for ledger confirmation
— Order becomes IN_PROGRESS
Funding typically takes 5-10 seconds. The order status progresses through ESCROW_FUNDING → ESCROW_FUNDED → IN_PROGRESS.
Releasing Funds
When the buyer approves the work, release funds to the seller:
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"}'
Releasing funds requires three separate blockchain transactions:
Step Transaction Signer 1 complete_escrowBuyer approves work 2 release_escrowPlatform authorizes release 3 Claim funds Seller receives USDC
{
"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:
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"
}'
After review, the platform resolves the dispute:
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"
}'
Step Transaction Signer 1 dispute_escrowBuyer opens dispute 2 resolve_disputePlatform refunds buyer
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:
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
});
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:
Stored in the database for fast operations:
{
"available": "100.00",
"reserved": "50.00"
}
— Can be used for new orders or withdrawn
— Locked for pending orders (before escrow funding)
Actual USDC in the user's Stellar wallet:
{
"stellar_address": "GCV24WNJ...",
"usdc_balance": "150.00"
}
Stage Available Reserved On-Chain Initial 100.00 0.00 100.00 After reserve 50.00 50.00 100.00 After funding 50.00 0.00 50.00 (in escrow) After release 50.00 0.00 50.00
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.
The platform wallet is a special Stellar account that:
— Required for release/refund
— Platform fees are sent here
— Only signs, doesn't custody
Configure it in your environment:
PLATFORM_USER_ID=usr_platform
Error Handling
Error Cause Solution ESCROW_ALREADY_EXISTSDuplicate creation attempt Check order status first ESCROW_NOT_FUNDEDTrying to release unfunded escrow Fund the escrow first INVALID_STATE_TRANSITIONWrong order state Follow the state machine INSUFFICIENT_FUNDSWallet doesn't have enough USDC Check balance before funding CONTRACT_ERRORStellar contract failure Check Trustless Work status
Handling Timeouts
Blockchain transactions can take time. Handle timeouts gracefully:
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:
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
— All signing happens server-side
— Prevent spoofed events
— Prevent duplicate funding
— Cross-check with Stellar explorer
Next Steps
Disputes Guide — Handle disagreements
Withdrawals Guide — Move funds off-platform
SDK Reference — Full SDK documentation