This is the public API/SDK guide for integrators. For internal implementation detail (Orchestrator state machine, signer roles), see docs/guides/escrow.md in the repo.
How blockchain-based escrow works in OFFER-HUB — fund locking, release flows, and dispute handling.
This is the public API/SDK guide for integrators. For internal implementation detail (Orchestrator state machine, signer roles), see docs/guides/escrow.md in the repo.
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.
OFFER-HUB uses Trustless Work smart contracts on the Stellar blockchain for non-custodial escrow. This means:
| Role | Description |
|---|---|
| Buyer | Funds the escrow; approves milestones; signs fund-release transactions |
| Seller | Receives funds; marks milestones as completed (serviceProvider role) |
| Platform | OFFER-HUB's signer; acts as disputeResolver — required for any dispute resolution |
| Arbiter | Trustless Work's smart contract; enforces that disputeResolver ≠ disputer on-chain |
The escrow contract follows the internal Escrow state machine below — the same canonical diagram used across the docs, matching docs/architecture/state-machines.md in the orchestrator repository exactly. The order around it follows the Order lifecycle.
| State | Description |
|---|---|
CREATING | Creating contract in Trustless Work |
CREATED | Contract created, not funded |
FUNDING | Funding contract |
FUNDED | Funds locked in contract |
RELEASING | Releasing funds to the seller |
RELEASED | Funds released |
REFUNDING | Returning funds to the buyer |
REFUNDED | Funds returned |
DISPUTED | Active dispute |
Order-level statuses (e.g. ESCROW_CREATING, ESCROW_FUNDING, ESCROW_FUNDED, IN_PROGRESS, CLOSED) belong to the Order state machine. The escrow machine above tracks the smart contract itself.
After reserving funds, create the escrow contract:
Response:
The contract deployment happens asynchronously. Listen for events or poll the order status.
Once the contract is created, fund it with USDC:
This triggers an on-chain USDC transfer from the buyer's invisible wallet to the escrow contract.
IN_PROGRESSFunding typically takes 5-10 seconds. The order status progresses through ESCROW_FUNDING → ESCROW_FUNDED → IN_PROGRESS.
When the buyer approves the work, release funds to the seller:
Releasing funds requires three separate Stellar transactions signed by different parties:
| Step | Contract call | Signer | Role in contract |
|---|---|---|---|
| 1 | changeMilestoneStatus | Seller | serviceProvider |
| 2 | approveMilestone | Buyer | approver |
| 3 | releaseFunds | Buyer | releaseSigner |
The seller signs first to mark the milestone as completed, then the buyer signs twice — once to approve and once to trigger the actual fund transfer. All signing happens server-side via the parties' invisible wallets.
If there's a problem, the buyer can request a refund through the dispute process:
After review, the platform resolves the dispute:
| Step | Contract call | Signer | Role in contract |
|---|---|---|---|
| 1 | disputeEscrow | Buyer | approver (disputer) |
| 2 | resolveDispute (100% to buyer) | Platform | disputeResolver |
Disputes require platform intervention. The smart contract won't transfer funds until the platform signs the resolveDispute transaction as disputeResolver.
Role separation is enforced on-chain. The disputeResolver must be a different party from the disputer. Because the buyer opens the dispute (step 1), only the platform wallet — never the buyer or seller — can sign the resolution (step 2). This prevents either party from unilaterally reclaiming funds.
The SDK simplifies escrow operations:
OFFER-HUB tracks two types of balances:
Stored in the database for fast operations:
Actual USDC in the user's Stellar wallet:
| 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:
Configure it in your environment:
| Error | Cause | Solution |
|---|---|---|
ESCROW_ALREADY_EXISTS | Duplicate creation attempt | Check order status first |
ESCROW_NOT_FUNDED | Trying to release unfunded escrow | Fund the escrow first |
INVALID_STATE_TRANSITION | Wrong order state | Follow the state machine |
INSUFFICIENT_FUNDS | Wallet doesn't have enough USDC | Check balance before funding |
CONTRACT_ERROR | Stellar contract failure | Check Trustless Work status |
Blockchain transactions can take time. Handle timeouts gracefully:
Escrow lifecycle events are delivered over the SSE event stream at GET /api/v1/events — there is no outbound webhook API:
Or with the SDK:
See Events (SSE) for the full event catalog, connection parameters, and replay support.
GET /api/v1/events; there is no outbound webhook API