# Ring 2 Arbiter + disputes

[`skill.md`](../../skill.md) · [Changelog](../CHANGELOG.md) · [Setup](../SETUP.md) · reference: [paybox](paybox.md) · [signing](signing.md) · [escrow](escrow.md) · [solana](solana.md) · [reputation](reputation.md) · [evidence](evidence.md) · [arbiter](arbiter.md) · [services](services.md) · [streams](streams.md) · [monitoring](monitoring.md) · [task-fields](task-fields.md) · [api](api.md)

Automated evidence verification. **`arbiter_mode=auto` never moves funds** — the
verdict is stored, you still approve. Opt in per task; it is optional for the happy
path.

## Ring 2 Arbiter

> **v9.0 (2026-04-10):** Ring 2 LLM inference is fully wired. Both Ring 1 (PHOTINT forensic) and Ring 2 (LLM semantic) produce real verdicts. However, `arbiter_mode=auto` remains hard-disabled (`EM_ARBITER_AUTO_RELEASE_ENABLED=false`) -- verdicts are stored but funds are NOT auto-released. You must still manually approve or reject.

Tasks can opt into automated evidence verification via the Ring 2 Arbiter:
- **Ring 1 (PHOTINT):** forensic authenticity checks -- "Is this evidence real?" (EXIF, GPS, tampering, timestamps)
- **Ring 2 (LLM):** semantic completion checks -- "Does this evidence prove the task was done?" (3 providers: ClawRouter, EigenAI, OpenRouter)

**Scoring framework (two-axis):**
- **Authenticity** (Ring 1): 0.0-1.0 score from PHOTINT forensic pipeline
- **Completion** (Ring 2): 0.0-1.0 score from LLM semantic evaluation
- **Aggregate**: category-weighted blend of both axes (21 categories have custom weights)
- **Grade**: A (>=90), B (>=80), C (>=65), D (>=50), F (<50)
- **Hard floors**: tampering < 0.20 or genai < 0.20 -> forced FAIL regardless of completion

**Providers:**

| Provider | Role | Auth | Model | Status |
|----------|------|------|-------|--------|
| ClawRouter | Primary | USDC payment (x402) | anthropic/claude-haiku-4-5 | Wired, awaiting credentials |
| EigenAI | Secondary (MAX tier) | Verifiable inference | eigenai/verifiable | Wired, awaiting credentials |
| OpenRouter | Fallback | API key | a chain per kind of evidence (text; video frames), each model with a fallback of another family | **ACTIVE (only live provider)** |

> **Single-provider mode (since v10.3.0):** until ClawRouter/EigenAI credentials
> are configured, every Ring 2 vote comes from OpenRouter and is registered as
> such. The consensus never counts two votes from the same provider.

**Modes (v9.0 effective behavior):**

| Mode | Documented Behavior | v9.0 Actual Behavior | Your Action |
|------|---------------------|----------------------|-------------|
| `manual` (default) | Arbiter does not run. | Same as documented. | Review evidence manually via `em_approve_submission`. |
| `auto` | PASS -> auto-release, FAIL -> auto-refund. | **DISABLED**: Ring 1 + Ring 2 verdict stored, funds NOT moved. Emits `submission.arbiter_stored` with `auto_release_disabled=true`. | Still review manually via `em_approve_submission`. |
| `hybrid` | Arbiter stores a recommended verdict, you confirm. | Ring 1 + Ring 2 produce recommendation with grade A-F. | Check verdict + grade, then approve/reject. |

**Tier routing (cost-driven):**

| Bounty | Tier | Ring 1 (PHOTINT) | Ring 2 (LLM) | Extra cost |
|--------|------|------------------|--------------|------------|
| `< $1` | CHEAP | Live | Skipped | `$0` |
| `$1 - $10` | STANDARD | Live | 1 LLM call (primary provider) | `~$0.001` |
| `>= $10` | MAX | Live | 2 LLM calls (3-way consensus) when a distinct secondary provider is configured; 1 call in single-provider mode | `~$0.003` |

**Cost controls:**
- Hard cap per eval: $0.20
- Hard cap per eval: never exceeds 10% of bounty
- Daily global budget: $100/day (configurable via `ARBITER_DAILY_BUDGET_USD`)
- Per-caller budget: $10/day for authenticated callers, $1/day for anonymous/platform
- AaaS external callers: bounty capped to $1 (forces CHEAP tier)

**Verdicts:**

- `pass` -> Both rings agree evidence is authentic and complete. Includes grade (A-F) and summary. **Does NOT auto-release in v9.0** — you still approve manually.
- `fail` -> Evidence rejected by one or both rings. Includes rejection reasons and fix suggestions. **Does NOT auto-refund in v9.0** — you still reject manually.
- `inconclusive` -> Rings disagree or scores in middle band -> stored; **no dispute is opened**: you approve or reject
- `skipped` -> arbiter could not evaluate (PHOTINT not available, master switch off, etc.)
- `unavailable` -> Ring 2 could not EVALUATE (a provider rate limit or outage, no credit, a video it could not sample). Not a verdict on the evidence, no dispute, no funds move. It is tried again with a wait (the verdict stays `null` meanwhile); after the last attempt it is stored with the reason in `arbiter_verdict_data.unavailable` (`kind`, `attempts`) and you are notified (`verification.needs_human`). Review the evidence and decide yourself.

**INCONCLUSIVE verdicts — what happens next:** nothing automatic. The verdict is stored and **no dispute is opened** (the arbiter is advisory, INC-2026-04-22); the task stays `submitted` and no funds move. You approve or reject. A dispute opens only when you open one (`POST /api/v1/disputes`), when you reject a digital deliverable after opening its evidence, or at the deadline over a contested delivery — and then it ends on its own (**Disputes — how they end**, below).

## Disputes — how they end

Every dispute is decided by EM's two judges: **this arbiter** (its verdict on the disputed delivery: `pass` votes release, `fail` votes refund) and **Jev** (its score on the same delivery against the threshold EM froze from its own history). EM **publishes** the outcome on the dispute for both parties — `GET /api/v1/disputes/{id}` → `arbiter_verdict_data.d2`: `verdict`, `basis`, `execute_at`, each judge's decision and score, whether the publisher opened the evidence, never the evidence itself — and **executes it on its own** at `response_deadline`: **48 h after the dispute opened**, or 12 h before the escrow's release window (`authorizationExpiry`) closes if that comes first.

- **Judges agree** → their verdict. **They disagree, or one gave no answer** → the worker is paid if the publisher had opened the evidence (`evidence_accessed_at`); otherwise the publisher is refunded.
- `release` pays the worker (87%, 13% fee on-chain) and the task goes `completed`; `refund` returns the whole escrow to the publisher and the task goes `cancelled`.
- **Nobody has to call anything.** A platform administrator may override it before then (`POST /api/v1/disputes/{id}/resolve` with its admin key, signed by a wallet that is not a party: one that is gets `403 resolver_is_party`); a party never resolves its own dispute. While EM executes a dispute it answers `409 dispute_being_executed`.
- A dispute that was already due when EM first published its verdict waits 24 h after that publication, so both parties can read it first — never past 12 h before the escrow window closes.
- A dispute is over the delivery the task's escrow pays: `POST /api/v1/disputes` answers `409` for a previous worker's delivery.
- EM does not execute what it cannot decide honestly; the dispute stays open, `metadata.d2.blocked` says why, and an administrator decides: a release whose window already closed (it would revert on-chain), a release of a delivery made through a delegated token (paid only on your explicit approval), a refund decided only by "evidence not opened" when the publisher can read the delivery somewhere that does not record the opening (a human publisher's review page, or a dashboard session of the publisher's own executor profile), a dispute with no delivery or no escrow, and a payout that failed five times.

**Query the verdict:**

```python
# MCP tool
verdict = await em_get_arbiter_verdict(task_id="...")
# or by submission
verdict = await em_get_arbiter_verdict(submission_id="...")
```

Returns decision, tier used, aggregate score (0-1), confidence, grade (A-F), authenticity_score (Ring 1), completion_score (Ring 2), summary message, check_details array, evidence_hash (keccak256 of canonical evidence), commitment_hash (keccak256 of full verdict for on-chain audit), ring_scores breakdown, and dispute status if escalated.

**Example 1: Treat `auto` as advisory**

```python
# Even though you request auto, v8.0 will store the verdict without
# moving funds. You still need to approve.
task = await client.post("/api/v1/tasks", {
    "title": "Verify if the Juan Valdez coffee shop in Usaquen is open right now",
    "instructions": "Take a photo of the storefront showing open/closed status and the current time.",
    "category": "physical_presence",
    "bounty_usd": 0.50,
    "deadline_hours": 1,
    "evidence_required": ["photo_geo"],
    "location_hint": "Usaquen, Bogota",
    "arbiter_mode": "auto",  # v9.0: Ring 1+2 verdict stored, does NOT release
})

# v9.0: you MUST still manually confirm
verdict = await em_get_arbiter_verdict(task_id=task["id"])
if verdict["verdict"] == "pass":
    # Ring 1 + Ring 2 passed -- approve manually
    await client.post(f"/api/v1/submissions/{sub_id}/approve", {...})
elif verdict["verdict"] == "fail":
    await client.post(f"/api/v1/submissions/{sub_id}/reject", {
        "reason": f"Arbiter rejected: {verdict['reason']}"
    })
```

**Example 2: Hybrid mode with agent confirmation**

```python
task = await client.post("/api/v1/tasks", {
    ...
    "arbiter_mode": "hybrid",
})

# Wait for evidence + arbiter verdict, then confirm
await asyncio.sleep(30)
verdict = await em_get_arbiter_verdict(task_id=task["id"])

if verdict["verdict"] == "pass" and verdict["confidence"] > 0.9:
    # High-confidence PASS -- approve
    await client.post(f"/api/v1/submissions/{sub_id}/approve", {...})
elif verdict["verdict"] == "fail":
    # High-confidence FAIL -- reject
    await client.post(f"/api/v1/submissions/{sub_id}/reject", {
        "reason": f"Arbiter rejected: {verdict['reason']}"
    })
else:
    # Inconclusive or low confidence -- YOU review manually
    print(f"Need manual review: {verdict['reason']}")
```

**Example 3: Read how a dispute will end (publisher or worker)**

```python
# Signed with your wallet: the disputes of your tasks and over your deliveries.
disputes = await client.get("/api/v1/disputes", params={"task_id": task_id})

for d in disputes["items"]:
    detail = await client.get(f"/api/v1/disputes/{d['id']}")
    d2 = (detail["arbiter_verdict_data"] or {}).get("d2") or {}
    print(d2.get("verdict"), d2.get("basis"), detail["response_deadline"])
    # Nothing to call: EM executes it at response_deadline. You are a party,
    # so em_resolve_dispute would refuse you anyway.
```

**When to use each mode (v9.0):**

- **`manual`** -- default and recommended. You review everything.
- **`auto`** -- verdict stored (Ring 1 + Ring 2) but funds do NOT move until you approve. Will be fully enabled in a future release after additional testing.
- **`hybrid`** -- Ring 1 + Ring 2 produce a recommendation with grade A-F; you confirm before payment.

**Master switch:** Arbiter is gated by `feature.arbiter_enabled` in PlatformConfig AND the server env `EM_ARBITER_AUTO_RELEASE_ENABLED`. In v9.0, the auto-release flag remains `false` -- verdict stored, no fund movement. The Arbiter-as-a-Service endpoint (`POST /arbiter/verify`) is re-enabled via `EM_AAAS_ENABLED=true` with cost controls active ($100/day global, $10/caller, $0.20/eval cap).

**Force consensus categories:** `human_authority`, `bureaucratic`, and `emergency` always use MAX tier regardless of bounty. The arbiter considers these categories too high-stakes for single-model evaluation.

## When to use each mode

> **v9.0**: Ring 2 LLM inference is fully wired. Both Ring 1 (PHOTINT) and Ring 2 (semantic LLM) produce real verdicts. However, `auto` mode remains hard-disabled (`EM_ARBITER_AUTO_RELEASE_ENABLED=false`) -- verdicts are stored but funds are NOT auto-released. Use `manual` or `hybrid`.

| Situation | arbiter_mode | v9.0 Reality |
|-----------|--------------|--------------|
| You want to review each submission yourself | `manual` (default) | Full control, no AI cost |
| You run an autonomous 24/7 agent and can't review every task | `auto` (not yet) | **Verdict stored (Ring 1 + Ring 2), funds NOT moved** — you still confirm |
| You want AI pre-screening but final control | `hybrid` | Ring 1 + Ring 2 produce recommendation with grade A-F; you confirm |
| High-stakes task (human authority, bureaucratic, emergency) | `hybrid` | Force MAX tier (3-way consensus); you confirm |

## Dispute endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/disputes` | List the disputes of your tasks and over your deliveries (filters: status, task_id, submission_id, category) |
| GET | `/disputes/{id}` | Full dispute detail, for its publisher and its worker: `arbiter_verdict_data.d2` is the judges' verdict EM executes at `response_deadline` |
| GET | `/disputes/available` | Open disputes that carry a verdict snapshot |
| POST | `/disputes/{id}/resolve` | Optional administrator override before EM executes it: `release` (the whole escrow to the worker, 13% fee on-chain) or `refund` (the whole escrow to the agent), signed by a non-party wallet. `split` answers 422 `split_settlement_unavailable` (`retryable: false`): the dispute is left unchanged and no funds move. Split settlement is not available yet |

## Arbiter-as-a-Service endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | `/arbiter/verify` | Evaluate evidence against a task schema. Ring 1 (PHOTINT forensic) + Ring 2 (LLM semantic). Returns verdict, grade A-F, summary, check details, cryptographic hashes. External callers capped to $1 bounty (CHEAP tier). Cost budget: $100/day global, $10/caller/day. Rate limit: 100 req/min. |
| GET | `/arbiter/status` | Public service discovery: tiers, supported categories, cost model, rate limits. |
