OFFER-HUB uses Trustless Work smart contracts on Stellar for non-custodial escrow. All flows ultimately resolve on-chain.
Complete guide to all escrow lifecycle flows — creation, funding, release, dispute, and refund.
This guide walks through every escrow flow in OFFER-HUB: from creating and funding a contract to releasing, disputing, and refunding. Each flow includes step-by-step diagrams and code examples.
OFFER-HUB uses Trustless Work smart contracts on Stellar for non-custodial escrow. All flows ultimately resolve on-chain.
Escrow and order lifecycles are governed by two canonical state machines (matching docs/architecture/state-machines.md in the orchestrator repository exactly). The Order state machine tracks the order lifecycle, while the internal Escrow state machine tracks the smart contract itself. The flows below map onto these two machines.
An escrow contract is created after an order is placed and the buyer's funds are reserved off-chain.
ORDER_CREATED → FUNDS_RESERVED)ESCROW_CREATING)CREATED; the order advances to ESCROW_FUNDINGResponse:
Contract deployment is asynchronous. Use waitForStatus or subscribe to the order.escrow_creating event rather than polling manually.
Once the contract is deployed, the buyer's reserved USDC is transferred on-chain into the smart contract.
IN_PROGRESSAlways include an Idempotency-Key when funding. If the request times out, retrying with the same key is safe and prevents double-funding.
| Stage | Available | Reserved | On-Chain (Wallet) | In Contract |
|---|---|---|---|---|
| After reserve | 50.00 | 50.00 | 100.00 | 0.00 |
| After funding | 50.00 | 0.00 | 50.00 | 50.00 |
Funds can be released in two ways depending on how the order is configured.
The buyer approves release after a deadline or after delivery confirmation within a time window.
For larger projects, create one order with multiple milestones. The milestones are tracked within the same order and share its escrow. Complete each milestone as work is delivered, then use the order resolution flow to release, refund, or dispute the order.
Milestones do not have separate order lifecycles or escrow contracts. Use GET /orders/{id}/milestones to list them and POST /orders/{id}/milestones/{ref}/complete to mark one complete within the parent order.
When the buyer approves the delivered work, funds are released to the seller in three on-chain transactions.
| Step | Contract call | Signer | Role in contract |
|---|---|---|---|
| 1 | changeMilestoneStatus | Seller | serviceProvider |
| 2 | approveMilestone | Buyer | approver |
| 3 | releaseFunds | Buyer | releaseSigner |
Response:
After release, the seller's internal balance is credited. They can withdraw to their external wallet at any time via the Withdrawals flow.
If the buyer believes the work was not delivered as agreed, they can open a dispute while the order is IN_PROGRESS.
DISPUTED (funds stay locked in the contract)Response:
Disputes can only be opened while the order is IN_PROGRESS. Once released or refunded, the order is final.
After a dispute is opened, the platform reviews evidence and resolves with a release (seller wins) or refund (buyer wins).
| Decision | Outcome | On-Chain Transaction |
|---|---|---|
FULL_RELEASE | Funds go to seller | resolve_dispute → release |
FULL_REFUND | Funds return to buyer | resolve_dispute → refund |
SPLIT | Custom split between parties | resolve_dispute → split |
Only the platform (using the master API key) can resolve disputes. This requires a co-signature from the Trustless Work smart contract arbiter.
A refund can result from dispute resolution or, in some configurations, from a direct cancellation before work begins.
Two on-chain transactions are required:
| Step | Contract call | Signer | Role in contract |
|---|---|---|---|
| 1 | disputeEscrow | Buyer | approver (disputer) |
| 2 | resolveDispute (100% to buyer) | Platform | disputeResolver |
Role separation is enforced by the smart contract. The disputeResolver must be a different party from the disputer. Because the buyer signs step 1, the platform wallet is the only valid signer for step 2. Neither the buyer nor the seller can unilaterally resolve a dispute to reclaim funds.
If the order is cancelled before funding (e.g., the seller cannot fulfill), the off-chain reservation is released with no blockchain transaction:
Cancellations before escrow funding are instant — no Stellar transaction required. The buyer's reserved balance is immediately returned to available.
Lifecycle events are delivered over the SSE event stream at GET /api/v1/events — there is no outbound webhook API. Subscribe to keep your application in sync:
| Error | Cause | Fix |
|---|---|---|
ESCROW_ALREADY_EXISTS | Contract already created for this order | Check status before calling create |
ESCROW_NOT_FUNDED | Release attempted before funding | Fund the escrow first |
INVALID_STATE_TRANSITION | Action not valid in current status | Follow the state machine |
INSUFFICIENT_FUNDS | Buyer wallet has insufficient USDC | Check balance before funding |
DISPUTE_ALREADY_OPEN | Duplicate dispute on same order | Resolve existing dispute first |
FUNDING_TIMEOUT | Stellar transaction not confirmed | Check order status — do not retry blindly |
Use idempotency keys on all mutating requests (fund, release, dispute). This allows safe retries without risk of duplicate transactions.