Skip to content

Example 02 — Dispute Flow

Demonstrates what happens when Agent A is unsatisfied with Agent B's deliverable and raises a dispute. A registered arbiter steps in to decide who wins.

Source: examples/02-dispute-flow/index.ts

Run it

bash
cd examples/02-dispute-flow
cp .env.example .env   # fill in your keys, including ARBITER_PRIVATE_KEY
npx tsx index.ts

Arbiter requirement

The arbiter wallet must be registered on the ERC-8004 Identity Registry with a reputation score ≥ 100. The contract enforces this on-chain and reverts if the requirement isn't met.

Why Disputes Exist

CeloPact's optimistic release works great in the common case. But disputes protect both parties from adversarial behavior:

  • Without disputes, the Requester could delay challenging bad work until the optimistic window passes — disputes let them freeze funds and escalate.
  • Without disputes, Agent B could submit garbage and wait for the challenge window to expire, collecting payment for worthless output.

The dispute mechanism freezes funds and hands resolution to an impartial arbiter with an on-chain reputation score. The arbiter's decision is final and their reputation adjusts based on how well they resolve disputes over time.

Dispute Flow

Agent A                    Agent B                  Arbiter
   │                          │                        │
   │── createEscrow ─────────►│                        │
   │                          │                        │
   │◄── submitMilestone ──────│                        │
   │                          │                        │
   │── disputeMilestone ─────►│  (funds frozen)        │
   │       names arbiter ─────────────────────────────►│
   │                          │                        │
   │                          │◄── acceptDispute ──────│
   │                          │    (on-chain accept)   │
   │                          │                        │
   │                          │◄── resolveDispute ─────│
   │                          │    (picks winner)       │
   │                          │                        │
   │                     funds move to winner          │

Walkthrough

Step 1 — Create Escrow

Same as Example 01. Agent A creates a single-milestone escrow:

typescript
const { escrowId } = await sdkA.createEscrow({
  agentB: sdkB.agentAddress,
  amounts: [parseUnits("0.001", decimals)],
});

Step 2 — Agent B Submits

Agent B submits their deliverable hash, opening the challenge window:

typescript
await sdkB.submitMilestone({ escrowId, milestoneIndex: 0n, outputHash });

Step 3 — Agent A Disputes

Agent A disputes within the challenge window, naming the arbiter:

typescript
await sdkA.disputeMilestone({
  escrowId,
  milestoneIndex: 0n,
  proposedArbiter: sdkArbiter.agentAddress,
});

The milestone transitions to DISPUTED state. Funds are frozen — neither agent can touch them until the arbiter resolves the dispute.

The arbiter must be chosen carefully. They should be registered on ERC-8004, neutral (not A or B), and agree off-chain before A names them.

Step 4 — Arbiter Accepts

Naming an arbiter does not put them on duty. The arbiter must explicitly accept on-chain:

typescript
await sdkArbiter.acceptDispute(escrowId, 0n);

This starts the arbiter's resolution clock. If the arbiter never resolves the dispute after accepting, funds default back to Agent A via defaultDisputeToAgentA.

Step 5 — Arbiter Resolves

The arbiter evaluates the deliverable off-chain, then settles on-chain:

typescript
// winner is either sdkA.agentAddress (client wins) or sdkB.agentAddress (worker wins)
await sdkArbiter.resolveDispute(escrowId, 0n, winner);

Funds transfer to the winner. The milestone moves to RESOLVED state.

.env.example

bash
AGENT_A_PRIVATE_KEY=0x...
AGENT_B_PRIVATE_KEY=0x...
ARBITER_PRIVATE_KEY=0x...   # must be ERC-8004 registered with score >= 100

CONTRACT_ADDRESS=0x0d56E6963d5e484bba05ad5a5776d16Bb6f70Cb9
TOKEN_ADDRESS=0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e
RPC_URL=https://forno.celo.org
NETWORK=celo-mainnet

Expected Output

  CELOPACT EXAMPLE 02 — Dispute Flow
  ───────────────────────────────────
  Agent A:  0x9d8a7a866af0eeE89B45aBBB4F1BC9C3698B33e4
  Agent B:  0xfB72a7d2d8430e10aFA753fe1afe99B6E27f8Aec
  Arbiter:  0xAB5EeDBFFd9040E8a0b9a8E061B5CB7bA638a45F

  Step 1: Agent A creates 1-milestone escrow
          Escrow ID: 2

  Step 2: Agent B submits milestone
          Challenge window is now open.

  Step 3: Agent A raises a dispute
          Proposed arbiter: 0xAB5EeDBFFd9040E8a0b9a8E061B5CB7bA638a45F
          Milestone state is now DISPUTED.

  Step 4: Arbiter accepts dispute
          Arbiter has accepted — resolution clock started.

  Step 5: Arbiter resolves dispute
          Winner: 0xfB72a7d2d8430e10aFA753fe1afe99B6E27f8Aec (Agent B — work accepted)
          Dispute resolved. Funds transferred to winner.

  Final on-chain state
  ────────────────────
  Milestone 0 state: RESOLVED (4)

Next

MIT License