Seller Integration Guide#
This guide covers everything a seller needs to integrate xenga escrow payments into an existing system.
Architecture overview#
Buyer Facilitator Server Blockchain
│ │ │
├─ POST /orders/:id/pay ──→│ │
│←── 402 + requirements ───┤ │
├─ sign ERC-3009 ─────────→│ │
│ ├─ verify signature │
│ ├─ createEscrowWithAuth() ──→│
│ │←── txHash + escrowId ──────┤
│←── 200 + payment ────────┤ │
│ │ │
│ [Seller delivers] │ │
│ │ │
├─ confirmDelivery() ──────────────────────────────────→│
│ [Dispute window runs] │ │
├─ releaseFunds() ─────────────────────────────────────→│
│ [or autoRelease after timeout] │
The facilitator server handles the xenga HTTP flow — it mediates between buyers and the blockchain, manages orders, and dispatches webhooks on escrow lifecycle changes.
Creating orders#
curl -X POST https://api.xenga.xyz/api/orders \
-H "Content-Type: application/json" \
-H "X-API-KEY: sk_live_abc123" \
-d '{
"title": "Premium Widget",
"description": "A high-quality widget",
"price": 25.00,
"serviceType": "marketplace",
"sellerAddress": "0xYOUR_SELLER_ADDRESS",
"terms": "Ships within 5 business days. 30-day return policy for defective items."
}'The sellerAddress is where USDC will be sent when the escrow is released. The optional terms field is hashed (keccak256) and stored on-chain as contentHash in the escrow struct, providing tamper-proof evidence of agreed terms.
Service types#
| Type | Release window | Dispute window | Auto-verify |
|---|---|---|---|
marketplace | 7 days (adjustable by reputation) | 3 days | No |
agent-service | 1 hour | 3 days | Yes (5s delay) |
Custom service types#
Register your own service type:
import { registerServiceType } from "@xenga/server";
registerServiceType({
name: "saas-subscription",
releaseWindow: 24 * 60 * 60, // 1 day
autoVerify: true,
description: "SaaS with 1-day release window",
adjustParams(params, reputation) {
// Trusted sellers get shorter window
if (reputation.sellerScore >= 80 && reputation.sellerConfidence === "high") {
return { ...params, releaseWindow: 12 * 60 * 60 }; // 12 hours
}
return params;
},
});Seller actions (on-chain)#
After a buyer pays and the escrow is active, the seller can:
Confirm delivery#
Signals that the service/goods have been delivered. Starts the dispute window countdown.
import { createEscrowClient } from "@xenga/client";
const client = createEscrowClient({
privateKey: "0xSELLER_PRIVATE_KEY",
serverUrl: "https://api.xenga.xyz",
escrowVaultAddress: "0xCONTRACT",
chainId: 84532,
});
await client.confirmDeliveryOnChain(escrowId);Voluntary refund#
The seller can refund the buyer at any time while the escrow is active. The buyer receives the full amount (including facilitator fee — the facilitator absorbs the cost).
await client.refundOnChain(escrowId);Check stats#
const stats = await client.getSellerStats();
console.log(`Total escrows: ${stats.totalEscrows}`);
console.log(`Completed: ${stats.completedCount}`);
console.log(`Disputed: ${stats.disputedCount}`);Webhooks#
Register webhooks to receive real-time notifications when escrow state changes.
Register#
curl -X POST https://api.xenga.xyz/api/webhooks \
-H "Content-Type: application/json" \
-H "X-API-KEY: sk_live_abc123" \
-d '{
"url": "https://yourapp.com/webhooks/xenga",
"secret": "whsec_your_secret_here_min16chars",
"eventTypes": ["escrow.created", "escrow.released", "escrow.disputed", "escrow.refunded"]
}'Handle#
app.post("/webhooks/xenga", (req, res) => {
// Verify signature
const signature = req.headers["x-webhook-signature"];
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");
if (signature !== `sha256=${expected}`) {
return res.status(401).send("Invalid signature");
}
const event = req.body;
switch (event.type) {
case "escrow.created":
// Start fulfillment
break;
case "escrow.released":
// Mark order as complete, reconcile payment
break;
case "escrow.disputed":
// Alert seller, prepare evidence
break;
case "escrow.refunded":
// Update order status
break;
}
res.sendStatus(200);
});Event types#
| Event | When |
|---|---|
escrow.created | Buyer's payment is locked in escrow |
delivery.confirmed | Seller confirmed delivery (dispute window starts) |
escrow.released | Buyer released funds to seller |
escrow.auto_released | Auto-release triggered after timeout |
escrow.disputed | Buyer filed a dispute |
escrow.resolved | Arbiter resolved the dispute |
escrow.refunded | Escrow refunded to buyer |
Fee configuration#
The facilitator fee is deducted from the seller's payout at settlement. The buyer always pays exactly the order price.
Buyer pays: $25.00 (order price)
Seller gets: $25.00 - fee
Fee: ($25.00 * 1%) + $0.05 = $0.30
Seller net: $24.70
On refund, the buyer gets the full $25.00 back. The facilitator absorbs the fee loss.
Configure via environment:
FEE_BPS— percentage in basis points (100 = 1%, max 1000 = 10%)FEE_FLAT_USDC— flat fee in USDC smallest unit (50000 = $0.05)FEE_RECIPIENT— address to receive fees (required when fees > 0)
Escrow lifecycle#
None → Active → DeliveryConfirmed → Completed (buyer releases)
│ │ AutoReleased (timeout)
│ └────────────→ Disputed ──→ Resolved (arbiter)
└───────────────────────────→ Refunded (seller/arbiter)
Timing:
- Auto-release from Active: after
releaseWindow + disputeWindow - Auto-release from DeliveryConfirmed: after
releaseWindowfrom creation ANDdisputeWindowfrom delivery confirmation - Dispute from DeliveryConfirmed: within
disputeWindowof delivery confirmation - Dispute from Active: between
releaseWindow - disputeWindowandreleaseWindow + disputeWindow
Custom database integration#
The server uses SQLite by default, but the PaymentDeps interface allows you to plug in any database. You need to implement:
getOrderById(id)— fetch an orderupdateOrderStatus(id, update)— update order after settlementclaimOrder(id)— atomically transitioncreated→pending_paymentrevertOrderClaim(id)— revert on settlement failure
See src/server/middleware/types.ts for the full PaymentDeps interface.
Transaction evidence (contentHash)#
When an order includes terms (or other content metadata), the server computes a contentHash (keccak256 of the terms string) and stores it on-chain in the escrow struct. This provides tamper-proof evidence of the agreed-upon terms at the time of escrow creation.
How it works:
- Seller includes
termswhen creating an order (e.g., delivery timeline, refund policy, service-level agreement). - When the buyer pays, the facilitator computes
contentHash = keccak256(terms)and passes it tocreateEscrowWithAuth(). - The
contentHashis stored immutably in the on-chain Escrow struct and emitted in theEscrowCreatedevent. - During a dispute, the arbiter can verify that the original terms match the on-chain hash, preventing either party from altering the agreement after the fact.
Best practices:
- Include clear, specific terms for every order (delivery timelines, quality expectations, refund conditions).
- For agent services, include the expected input/output format and SLA.
- For marketplace orders, include shipping terms and return policy.
- The
contentHashisbytes32(0)when no terms are provided -- orders without terms still function normally but lack on-chain evidence for disputes.
Verifying a contentHash off-chain:
import { keccak256, toBytes } from "viem";
const terms = "Ships within 5 business days. 30-day return policy.";
const hash = keccak256(toBytes(terms));
// Compare with on-chain escrow.contentHashSecurity considerations#
- Operator wallet: The
PRIVATE_KEYis a hot wallet. In production, consider KMS/HSM solutions. - Arbiter trust: By default, the operator is also the dispute arbiter. Configure
ARBITER_ADDRESSseparately if needed. - API keys: Always set
API_KEYSin production to prevent unauthorized order creation. - Fee absorption: On refund, the facilitator absorbs the fee. Factor this into pricing.