VerdiktCourt is a reusable on-chain arbitration primitive. Inherit VerdiktConsumerBase, implement _settle, and your contract or agent can open a dispute, get a byte-identical AI verdict, and settle it programmatically — no admin, no human in the loop.
The Solidity side is a Foundry library: VerdiktConsumerBase plus the IVerdiktCourt interfaces. Install the repo, import the base and the Verdict enum, and you are wired to the court.
$ forge install Ridwannurudeen/verdikt
import {VerdiktConsumerBase} from "verdikt/VerdiktConsumerBase.sol"; import {Verdict} from "verdikt/interfaces/IVerdiktCourt.sol";
The complete reference integration — a working two-party escrow on the AI jury, shown verbatim from the repo and validated end-to-end in test/SimpleEscrow.t.sol. Graded SPLIT and UNDECIDABLE verdicts are handled for free by the base.
VerdiktConsumerBase(court_) wires your contract to the shared court: the onlyCourt callback guard, the caseId↔ref mapping, and fee handling all come with it.
createDeal(payee) escrows msg.value against a deal id — plain contract state, nothing court-specific yet.
Either party calls dispute(id, evidence). The base's _openDispute quotes the court fee, opens the case, maps caseId↔id, and credits any overpayment as a pull-refund.
The court finalizes and calls back into _settle(ref, verdict) — the one function you implement. _payeeShareBps turns the label into a basis-point share: PAYEE 10000, SPLIT graded, PAYER and UNDECIDABLE 0.
Funds are credited to a pending[] ledger and each party calls withdraw() — so a reverting recipient can never brick finalize().
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {VerdiktConsumerBase} from "../VerdiktConsumerBase.sol"; import {Verdict} from "../interfaces/IVerdiktCourt.sol"; contract SimpleEscrow is VerdiktConsumerBase { struct Deal { address payer; address payee; uint256 amount; bool settled; } mapping(uint256 => Deal) public deals; mapping(address => uint256) public pending; uint256 public nextId = 1; constructor(address court_) VerdiktConsumerBase(court_) {} function createDeal(address payee) external payable returns (uint256 id) { require(msg.value > 0 && payee != address(0) && payee != msg.sender, "bad deal"); id = nextId++; deals[id] = Deal(msg.sender, payee, msg.value, false); } function dispute(uint256 id, string calldata evidence) external payable { Deal storage d = deals[id]; require(msg.sender == d.payer || msg.sender == d.payee, "not a party"); require(!d.settled, "settled"); _openDispute(id, evidence, msg.sender); } function _settle(uint256 ref, Verdict verdict) internal override { Deal storage d = deals[ref]; require(!d.settled, "settled"); d.settled = true; uint256 toPayee = (d.amount * _payeeShareBps(ref, verdict)) / 10000; pending[d.payee] += toPayee; pending[d.payer] += d.amount - toPayee; } function withdraw() external { uint256 amt = pending[msg.sender]; require(amt > 0, "nothing to withdraw"); pending[msg.sender] = 0; (bool ok,) = msg.sender.call{value: amt}(""); require(ok, "withdraw failed"); } receive() external payable {} }
VerdiktConsumerBase handles the boilerplate every consumer otherwise repeats. The court calls back through onVerdict, which the base guards with onlyCourt and routes to your _settle.
Every signature below is the real one from src/interfaces/IVerdiktCourt.sol. Nothing is invented.
quoteOpen() returns the exact fee; pass it as msg.value to openCase. The court convenes the trial panel (5 agents) and returns a caseId. Quotes change with panel pricing — read quoteOpen() at call time, never hardcode the fee.
The panel callback sets status to Ruled and fills verdict + rulingTime. Read it with getCase(caseId). If the verdict is SPLIT, read splitBps(caseId) — a graded court may return 2500 / 5000 / 7500 rather than assuming 50/50.
While status == Ruled and before appealDeadlineOf(caseId), the case's original consumer can call appeal{value: quoteAppeal(caseId)}(caseId, newEvidence). A larger panel (9 agents) re-tries with the appended evidence.
Once the appeal window has passed, anyone — a keeper, the counterparty, an agent — calls finalize(caseId). The court flips status to Final and invokes your onVerdict(escrowRef, verdict). Permissionless, so no human is needed to settle. If the panel errors, the consumer can retry(caseId) with a fresh deposit.
The SDK is a thin viem client. createVerdiktClient reads cases and subscribes to verdicts; createVerdiktAgent gives a wallet-backed agent the court actions — appeals, finalize, and a decentralized keeper sweep. Both ship in sdk/.
import { createVerdiktClient, payeeShareBps, VERDICT } from "verdikt/sdk/index.mjs"; const verdikt = createVerdiktClient({ publicClient, court: "0x..." }); const fee = await verdikt.quoteOpen(); const c = await verdikt.getCase(1); // { verdictLabel, statusLabel, ... } const bps = await payeeShareBps(verdikt, 1); // graded-SPLIT aware share const unwatch = verdikt.watchVerdicts((v) => console.log(v.caseId, v.verdict, v.receiptId) );
import { createVerdiktAgent } from "verdikt/sdk/agent.mjs"; const agent = createVerdiktAgent({ publicClient, walletClient, account, court: "0x..." }); // finalize every case whose appeal deadline has passed await agent.runKeeper(); // contest a ruling (auto-quotes the larger panel) await agent.appeal(caseId, "new evidence"); // settle a case AND claim the keeper bounty await agent.finalizeForBounty(bounty, caseId);
The read client exposes COURT_ABI, quoteOpen, quoteAppeal, getCase, splitBps, payeeShareBps and watchVerdicts. The agent adds appeal, finalize, runKeeper and finalizeForBounty — finalize is permissionless, so anyone can run a keeper and earn the bounty.
The full stack is deployed on testnet. Pass the VerdiktCourt address to your constructor — or to createVerdiktClient({ court }) — and verify each address on the explorer.
chain id 50312 · rpc https://api.infra.testnet.somnia.network/
Inherit the base, implement _settle, point at the live court — six protocols already settle here.