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#

TypeRelease windowDispute windowAuto-verify
marketplace7 days (adjustable by reputation)3 daysNo
agent-service1 hour3 daysYes (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#

EventWhen
escrow.createdBuyer's payment is locked in escrow
delivery.confirmedSeller confirmed delivery (dispute window starts)
escrow.releasedBuyer released funds to seller
escrow.auto_releasedAuto-release triggered after timeout
escrow.disputedBuyer filed a dispute
escrow.resolvedArbiter resolved the dispute
escrow.refundedEscrow 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 releaseWindow from creation AND disputeWindow from delivery confirmation
  • Dispute from DeliveryConfirmed: within disputeWindow of delivery confirmation
  • Dispute from Active: between releaseWindow - disputeWindow and releaseWindow + 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 order
  • updateOrderStatus(id, update) — update order after settlement
  • claimOrder(id) — atomically transition createdpending_payment
  • revertOrderClaim(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:

  1. Seller includes terms when creating an order (e.g., delivery timeline, refund policy, service-level agreement).
  2. When the buyer pays, the facilitator computes contentHash = keccak256(terms) and passes it to createEscrowWithAuth().
  3. The contentHash is stored immutably in the on-chain Escrow struct and emitted in the EscrowCreated event.
  4. 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 contentHash is bytes32(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.contentHash

Security considerations#

  • Operator wallet: The PRIVATE_KEY is a hot wallet. In production, consider KMS/HSM solutions.
  • Arbiter trust: By default, the operator is also the dispute arbiter. Configure ARBITER_ADDRESS separately if needed.
  • API keys: Always set API_KEYS in production to prevent unauthorized order creation.
  • Fee absorption: On refund, the facilitator absorbs the fee. Factor this into pricing.