Calling the service as raw HTTP JSON-RPC
Dispute templates
Your partner page
Skill document
The four steps, and the tool for each
tools/list is the source of truth for what your key can call, not this table. Read it on connect and build against what is in it. Read each tool’s input schema from tools/list too, for the exact spelling of its arguments: the field tables below say what a call carries, not how the keys are typed.
All three write tools are granted per key, by one Preset and together. dispute_create, evidence_pin and evidence_submit stand or fall as a set: if your key has no Preset all three are absent from tools/list, and calling one returns a JSON-RPC -32601 Method not found rather than a refusal you can read. That is a different condition from a Preset that only simulates, which is covered under What comes back and again under Steps 2 and 3.
Deployment names, tied to the value you pass
You do not passchain to dispute_create — your Preset fixes it, along with the court, the juror count and the template. A partner holds one API key per deployment, and which key you send decides where the dispute is created; your partner page lists your keys. For a key that can create disputes, the instructions string you receive when you connect names the chain, court and juror count your Preset files with, and says whether it broadcasts or only simulates. Read it on connect and believe it over any document, this one included.
Both deployments are exercised in that order: a dispute is created on the Testnet (arbitrum-sepolia-testnet), checked in the Court interface, and only then created on Beta (arbitrum-one).
The clock
A Kleros v2 dispute walks through periods whose lengths are properties of the court, not of the dispute:instructions string name yours — and it is not configured identically on the two deployments. Read with court_get on 2026-09-10:
timesPerPeriod, in seconds, ordered [evidence, commit, vote, appeal]. Read them at run time; do not hard-code them. They are governance parameters, they differ per court and per deployment — as the table shows — and the court your Preset files into is named in your instructions string. Note that the court ids differ too, so a court id is not portable between the two deployments.
Three things that follow, and matter more than the numbers:
The evidence period is a floor, not a window you can extend
The evidence period is a floor, not a window you can extend
arbitrum-one) is the shortest the dispute can stay open to evidence, and nothing you can call makes it longer. Budget against the shorter of the two figures, so the same code works on both deployments.A period can close before its deadline
A period can close before its deadline
deadline as an upper bound on the current period and poll status rather than modelling the clock.What one real dispute actually did
What one real dispute actually did
arbitrum-sepolia-testnet, court 8) on 2026-09-10, with three jurors: creation to status: "Executed" with a final ruling took about 66 minutes, against configured periods summing to well over two hours. Both halves of the rule above showed up in the same dispute — the commit and vote periods closed early once the jurors had acted, and the dispute then sat in Vote with its vote deadline already in the past, because a period does not advance itself when the clock runs out. Plan for a wide range, poll status, and never infer the period from the time.Step 1: Create the dispute
aliases is the only argument, and the only thing that varies between one of your disputes and the next. Its slot names are your Preset’s own — your partner page lists them and says who each party is, and your instructions string lists them too.
aliases is required — send it even when it is empty. dispute_create names it in the required array of its input schema, so "arguments":{} is refused with ALIASES_REQUIRED and nothing is created. To create a dispute with no party named, send "arguments":{"aliases":{}}. Naming no parties is legitimate; it just has to be something you say rather than something you leave out.- Omit a party you do not know. An omitted slot is simply absent from the filed template, which is how “unknown” is written. A zero address or any other placeholder is refused, not filed. That is per slot, and separate from the rule above: the
aliasesobject itself is always sent. - You cannot override the court, the juror count, the template or the deployment. Your Preset fixes them. Passing them as arguments is ignored, not honoured.
- The parties are labels for jurors, not permissions. Naming an address does not give it any standing, and evidence can be submitted by anyone.
What comes back
The result is the filing tool’s own JSON payload, passed through with three changes: upstream’smessage field is removed, a mined result gains localDisputeID or localDisputeIDUnavailable
(The two dispute ids), and an unknown result is prefixed with a warning,
noted below. The field to branch on is status:
reverted status. A creation transaction that was mined and failed arrives as a refusal, TRANSACTION_REVERTED with isError: true, never as a result to branch on — see When something goes wrong. So a result without isError is always one of the three rows above.
A failure comes back as isError: true with text shaped CODE: message — for example INSUFFICIENT_BALANCE: …. There is no machine-readable code field to switch on; match a short leading phrase at most, and expect the prose to move between releases. When a failure text says the transaction may have been sent, treat it exactly like unknown above.
Make one write at a time. Steps 1, 2 and 3 run one at a time per key (strictly, per signer your key’s Preset names), so your Beta and Testnet keys do not block each other. A write sent while another of yours is still running comes back at once as isError: true with text beginning WRITE_IN_PROGRESS: . Nothing was sent: wait for the other write to return, then call again. It is never queued for you, so a client that makes tool calls in parallel must serialise these three. Reads such as dispute_get are never held back.
unknown carries a prefix. That one result is prefixed with a plain-English warning before the JSON, so JSON.parse on the whole text throws. Every other status is bare JSON. If you parse defensively, parse the last JSON object in the text rather than the whole of it.
A simulation cannot be escalated into a filing by you. Whether your Preset broadcasts is set on this service, not passed as an argument. If a real filing is wanted and your instructions say you are in simulation, say so and let a human arrange it.
The two dispute ids
The dispute id arrives undercoreDisputeID — that exact casing, as a string, at the top level of the payload. Measured on the first broadcast run, 2026-09-10; there is no id and no disputeId, and nothing is nested. It is the id the Court interface shows and the id every read below is keyed on, with one exception you must read before you rely on it: Verifying a submission.
A mined payload carries a second identifier, and you must keep it. localDisputeID is the arbitrable contract’s own id for the same dispute — the id evidence_list is keyed on, and the one nothing else here returns (Verifying a submission).
- It is decoded from the creation transaction, so it is on
minedresults only: asimulatedfiling created no dispute to name, and anunknownone is reported before the receipt is read at all, so it carries atxHashand nocoreDisputeID— and without that number there is nothing to match the creation event against. Do not expect to recover the field on anunknownby re-reading the receipt yourself against the id you think it got. - When the receipt cannot be read back, that same payload carries
localDisputeIDUnavailablewith the reason instead — never a guessed number, so branch on the field being present rather than on its value. - The template id is still not returned. If a filing comes back
unknown, thetxHashis your only handle; resolve the dispute from it before doing anything else, and never by filing again.
Steps 2 and 3: Evidence, and the two tools that carry it
Evidence is two calls, and only the second one touches a chain.evidence_pin publishes one
Attachment to IPFS over HTTP — no wallet, no signer, no gas — and hands back its URI.
evidence_submit then records an Evidence document — a name, a description and optionally that
URI — against the dispute in a transaction of its own, separate from the one that created it, so
the jurors deciding it read your account of what happened. Both are granted by the same Preset that
grants dispute_create: if step 1 is in your tools/list, these two are beside it.
evidence_pin answers checked or published and nothing else — a vocabulary of its own, sharing
no value with either other tool, so do not wait for a pin to say mined. evidence_submit answers
simulated, mined or unknown, which is exactly step 1’s list, and on both a mined-and-failed
transaction is a refusal rather than a status. So a branch
written for step 1 can be reused for step 3, and a branch written for either is wrong for step 2.
Sequencing: publish the Attachment before you file
Pin first (evidence_pin)
evidence_pin takes no dispute id.Then create (dispute_create)
Then submit (evidence_submit)
- The evidence period starts when the dispute is created, and on Beta (
arbitrum-one) it is a floor of ten minutes — see The clock. Nothing you can call extends it. So run step 2 before step 1:evidence_pintakes no dispute id and nothing about it is dispute-specific, so a URI already in hand means none of that ten minutes goes on the upload. - A late submission is not refused. There is no period gate on chain: an Evidence document submitted after the evidence period closes is still accepted and still indexed. What you lose is readers, since jurors who have voted do not go back. A missed window is a reason to submit anyway and say so, not a reason to give up on the record.
Step 2: Publish the Attachment
data: URI: both are refused rather than decoded leniently, because decoding them would publish bytes you did not send. No URL you supply is fetched — there is no argument that takes one.pdf, png, json. One to ten lowercase letters or digits. It is not a filename: no filename is sent, because an Evidence document has no field for one.published result comes back — that check is always on and is not a Preset’s choice. It proves the
upload landed intact; it does not prove you sent the whole file, so if the bytes could have been
truncated before they reached us, compare cid against your own.
- There is no size limit in this tool’s schema, deliberately. The ceiling belongs to the
upstream endpoint and it is measured against the whole realised upload request rather than the
file, so it moves. Too large a file comes back as
FILE_TOO_LARGE, which names the limit that actually applied. Do not hard-code a number from anywhere, this document included. Note that base64 costs about a third on top of the file’s own size, and it is the encoded form that travels in your request body. The request limit is sized to admit every file the upload would accept, so a file just over the ceiling getsFILE_TOO_LARGE; only one far over it meets the HTTP413in When something goes wrong first, with no result to parse. Handle both. - Retrying a failed pin is safe. The same bytes give the same CID, nothing is spent and nothing is on a chain. Nothing is deduplicated either, so a retry really does re-upload — that costs time, not money or a second identity for the file.
Step 3: Submit the Evidence document
coreDisputeID. It is not the id evidence_list takes; see Verifying a submission. Digits only. Another real dispute’s id is accepted, indexed and wrong, and nothing on chain refuses it, so confirm the number before you call.@, or equal to -, is recorded exactly as you wrote it.fileUri./ipfs/<cid> URI of one Attachment — what step 2 returned, or one you pinned yourself. Recorded as given and never fetched, so it must already resolve for a juror. At most 1024 characters, with no whitespace, no control characters and no leading -.extension. Read by the subgraph only, and refused with FILE_URI_INVALID if you send it without fileUri — it describes that Attachment and means nothing alone.reverted is not a status on this tool, just as it is not on step 1. A submission that was mined and failed arrives as a refusal named TRANSACTION_REVERTED with isError: true, not as an ordinary result you branch on.
Branch on isError first on every tool here: a refusal is CODE: message as plain text, so parsing it as JSON throws. On steps 1 and 3 alike, the absence of isError means the transaction is not known to have reverted: simulated sent nothing and mined succeeded, but unknown has no receipt, so it cannot say either way.
unknown is prefixed with a plain-English warning before the JSON here exactly as it is on step 1, so JSON.parse on the whole text throws. Every other status on both tools is bare JSON; parse the last JSON object in the text if you parse defensively.
A duplicate submission is untidy rather than harmful, which is the whole difference from step 1: it costs gas and shows the jurors the same document twice, but it pays no arbitration fee and creates no second dispute. That makes an uncertain outcome here cheaper than step 1’s unknown, but no read settles it: escalate with the txHash, and do not submit again.
This spends gas, and no cost ceiling bounds it
This spends gas, and no cost ceiling bounds it
name (1 KiB) and description (64 KiB), which are there to stop a runaway rather than to price anything. Keep description to the argument you want read.Anyone may submit, including the counterparty
Anyone may submit, including the counterparty
evidence_list reads back what landed
evidence_list reads back what landed
evidence_list reads back what landed, whoever submitted, with the IPFS content resolved. It is in your tools/list today and needs no Preset. But it is keyed on the localDisputeID from step 1 rather than on the id you just submitted against, so read the next section before treating its answer as verification.Verifying a submission needs the other dispute id
The two ids are equal on Beta (arbitrum-one), ethereum and gnosis as a matter of those
deployments’ history rather than of the protocol, so code checked only against Beta passes and then
fails here. The first production dispute created through this service, on 2026-09-11, is one more
case where they are equal, which is what makes Beta a misleading place to test this rather than a
reassuring one. The equality ends for every later id the first time any other arbitrable
creates a dispute there, with no change on either side and no error to catch.
Three rules follow, and they override anything else in this document:
- An empty
evidence_listis not proof of absence. It is equally the symptom of the wrong id, and it arrives with a suggested command line asserting “no evidence submitted yet” as fact.dispute_get’sevidenceCountisnullon the same dispute, so it cannot contradict it either. - Never submit again because a list came back empty. That is the one inference this asymmetry makes unsafe, and it is the inference the wording invites.
- One payload carries the arbitrable’s id, and only one. A
minedstep 1 returns it aslocalDisputeID, decoded from the creation transaction on your behalf. Step 3 does not echo it,dispute_getanddispute_briefdo not carry it, and a dispute you did not create through this service never gave you one. Keep it with thecoreDisputeIDfrom the same result; if you do not have it, treat a submission as unverified and say so, rather than reporting it absent.
evidence_list reads an
index rather than the chain, so an empty answer shortly after a submission is weak evidence of
absence. The strongest confirmation a submission landed is still step 3’s own mined result and
its txHash.
The translation is upstream’s to do and is reported against AgentKit. localDisputeID is this service’s answer in the meantime, and it stays useful either way.
Step 4: Poll for the first-round ruling
dispute_get reads the arbitrator contract live on every call, so it carries a ruling for a dispute that is still in progress — you do not have to wait for execution, and you do not have to care about appeals to get a number.The shape of the response
dispute_get answers with a paged envelope, not a bare row. The dispute is the first element of
items, and the fields below are read off that, not off the top level:
.status off the parsed object gets undefined. Read items[0].status, and
check chainStatus when a field you expected is missing.
The fields that matter
Evidence, Commit, Vote, Appeal, Executed. This is your state machine.true means no option leads yet. This is the field that makes ruling interpretable.true.ruled means on this row. On dispute_get it is always onchain — read straight from the arbitrator, which is why ruled is trustworthy here. It reads period-reached on dispute_list, where it is not.ruling. Convenience only — do not parse it.1 while the first round is live. Above 1 means an appeal was funded.null when the underlying value is absent — do not read its presence as proof the dispute is still running, and do not read 1970-01-01 as anything but an unset value.The trap: ruling: "0" means two different things
arbitrum-one on 2026-09-10, in another court, and are shown for shape. 0 is both “refuse to arbitrate” and “nobody is ahead yet”, and rulingLabel renders the refuse-to-arbitrate wording in both cases.
tied is the discriminator. A client that reads ruling without tied will report a vote that has not happened as a juror refusal.- Poll with
dispute_get, never withdispute_list. The list tool is a subgraph snapshot, it can lag the chain, and it does not carrytiedat all — the exact field you need. - A tie survives to the end. If the appeal period expires with the round still tied, the ruling finalises as
0. Your handling of “no majority” is not a transient state, it is a real outcome.
The stop condition
You are done with the first round whenstatus becomes Appeal: that period opens once the vote period has closed, so the majority you read there is the first round’s verdict.
Cadence: one call every 30 seconds is plenty
429 carries no Retry-After, and the dispute will not move faster than the chain.Cap the loop on wall-clock time
Appeal well past the sum of the court’s periods, alert a human rather than polling forever — a dispute stuck in Evidence usually means jurors have not all been drawn.Record roundCount next to the ruling
What each ruling value means
ruling is a number; what it decides is your template’s. 0 with tied: false is always a
refusal to arbitrate, and every other value is the template answer whose id carries the same
number — 1 is 0x01, 2 is 0x02, and so on. Your partner page has the table for your
template, with the outcome each value stands for.
Treat that table as the expectation, and rulingOptions on the dispute itself as the truth: match your handler on the value each option carries. Confirm the mapping once, on the first Testnet (arbitrum-sepolia-testnet) dispute, before wiring it to money.
Ignoring appeals: what you are actually choosing
Reading the first-round majority and stopping is a legitimate choice. Be explicit about what it means:- The dispute keeps running without you. If an appeal is funded,
roundCountincrements andstatusgoes back throughCommitandVotewith more jurors, and the final on-chain ruling may differ from the number you recorded. - Nothing calls back into your system either way. These disputes are created through the generic Kleros arbitrable, so there is no
rule()callback into your contracts: whatever settlement follows is your own action, taken on the number you read. - If you later want the final outcome, the same
dispute_getcall gives it:ruled: truewithstatus: "Executed", andoverridden: trueif a parent court changed it on appeal.
When something goes wrong
- General
- Step 1: Create
- Step 2: Pin
- Step 3: Submit
isError first and escalate the text — do not pattern-match a code you have not been given a meaning for. On an unrecognised code, retry only step 2: a repeat pin of the same bytes publishes the same Attachment and spends nothing, while a repeat of step 1 or step 3 spends money against an outcome you have not established.
Before you go live
Read tools/list on connect and build against it
required arrays: dispute_create requires aliases even when it is empty, and a client that omits it is refused with ALIASES_REQUIRED on every call.Check whether your Preset broadcasts
instructions string says simulation, step 1 creates nothing and there is no dispute for step 4 to read, step 2 checks the file and publishes nothing, and step 3 records nothing. Arranging a broadcasting Preset is a human step on this side, not an argument you can pass.Rehearse on the Testnet first
arbitrum-sepolia-testnet) and look at the dispute in the Court interface: the template renders there, and one that reads badly to a juror is much cheaper to find before Beta (arbitrum-one) than after.Confirm on that first real run
rulingOptions values map onto your three answers the way your partner page’s ruling table expects (Step 4). The mined payload itself has now been read on a real filing — a Testnet (arbitrum-sepolia-testnet) dispute carried to a ruling — and it carries both identifiers: coreDisputeID, the number the Court interface shows, and localDisputeID, the one evidence_list keys on. Read the second from the creation result and keep it; no other call here returns it.