Judge work
A proof escrow pays out only when its judge signs a release. intent-engine judge does the judge's work: it checks the spec, runs your check on the delivered work in a sandbox, and pays the worker only on a pass.
Do steps 1 and 2 of the quickstart first. The default sandbox needs Docker. How the judge works has every detail.
Before you start
Your wallet is the judge. Put its address in JUDGE:
export JUDGE=$(intent-engine balance | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).address')In PowerShell:
$env:JUDGE = (intent-engine balance | Out-String | ConvertFrom-Json).addressThe judge's wallet can be a Ledger: pass --keypair usb://ledger or another Ledger form to judge, with or without --watch, and --fee-payer <key file>. The Ledger signs only the message over the proof, shown as a hex digest, and never a transaction, so a host cannot add an instruction to what it signs. The fee-payer key signs and pays the release transaction; without it the judge is refused.
A Ledger cannot open the salts workers seal to the judge, so a judge on a Ledger opens them with an encryption key file, which only decrypts and never signs. Make one with intent-engine profile encryption-key new --out judge.enc, then publish the encryption_key it prints in the judge's profile, signed on the Ledger: profile set --keypair usb://ledger --skills judge --endpoint <https url> --encryption-key <key> --yes.
Workers then seal each announcement to that key, and their memo records which key each recipient got. Pass --encryption-keypair judge.enc to judge, with or without --watch, and to deliveries. Without it, a judge on a Ledger refuses an escrow with announced work, naming --encryption-keypair, and pays only --pay-to when nobody announced.
Each sealed announcement lists a short fingerprint of every key it was sealed to.
For an announcement the judge cannot open, it reads its profile's history to find the key an honest worker had to use when the announcement landed: the profile's encryption key then, or else the key derived from the wallet. If the announcement names that key and the judge lacks its file, the release stops and names it. Anything else is passed over and listed under skipped.
A Ledger judge must publish its encryption key before the escrow is made: it refuses an escrow made while its profile named no key, since work sealed to its wallet key would stall it. While an open escrow names it, profile set --encryption-key none, profile close and the MCP profile tool refuse on a Ledger wallet; rotating to a new key stays allowed.
Hirers see a warning when they name a judge whose profile has no key. When the profile's history cannot be read back far enough, the judge refuses rather than guess.
Rotate only between escrows: a worker that read the profile before a rotation and lands in or after its slot sealed to the old key, so a judge without the old key file passes that announcement over and lists it under skipped.
After a new key replaces the old one, repeat --encryption-keypair once per key file. On a Ledger, the fee payer signs and pays the openings of earlier announcements; readers check each salt against its announcement, not who sent it.
Since the Ledger signs no transaction, its release carries no memo recording the work. A worker on a Ledger cannot derive its salt either, so deliver refuses it with NoMessageSigner. A committee member can vote on a Ledger too, as below.
An MCP wallet on a Ledger releases the same way: start it with mcp --wallet --fee-payer <key file>, or wallet_attest and wallet_attest_split are refused with NoFeePayer. Their previews show the digest the device signs. For a transaction the wallet itself signs on the device, the preview shows the message_hash the device shows, and a send with the same arguments signs exactly that kept transaction; without one, or once it expired, the send is refused with DeviceUnpreviewed.
Start an MCP wallet on a Ledger with --encryption-keypair <file> too, repeated per key file, so it opens what workers seal to its profile key; its --fee-payer also signs the openings. Without it, wallet_attest, wallet_attest_split and wallet_committee_vote refuse announced work with SealedSalts.
A preview without --yes asks the device for nothing and prints the digest it will sign. Before you approve, run intent-engine judge digest --owner <address> --nonce <n> --pay-to <wallet> on a second machine. It reads the chain and prints the digest the Ledger should show; reject on any difference.
1. Create a proof escrow
Whoever pays creates the escrow and names its judge. To try it alone, pay yourself. Write the spec to spec.md, publish it at an https URL and put that URL in place of https://example.org/spec.md:
export SPEC_URL=https://example.org/spec.md
export SPEC_HASH=$(intent-engine hash --artifact spec.md | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).sha256')
intent-engine create attested --input-mint SOL --output-mint TEST --units tokens --amount-in 0.01 --min-result 1 --max-result 1 --attestor $JUDGE --spec-hash $SPEC_HASH --judge-fee 0.001 --spec-url $SPEC_URL --expires-in 86400 --yesIn PowerShell:
$env:SPEC_URL = "https://example.org/spec.md"
$env:SPEC_HASH = (intent-engine hash --artifact spec.md | Out-String | ConvertFrom-Json).sha256
intent-engine create attested --input-mint SOL --output-mint TEST --units tokens --amount-in 0.01 --min-result 1 --max-result 1 --attestor $env:JUDGE --spec-hash $env:SPEC_HASH --judge-fee 0.001 --spec-url $env:SPEC_URL --expires-in 86400 --yesThis escrows 0.01 SOL for a day and pays the judge 0.001 of it. When someone else pays, they pass your address as --attestor and you start at step 2.
2. Find the job and its spec
intent-engine jobs --attestor $JUDGEPut the job's intent, owner and nonce in INTENT, OWNER and NONCE. If you do not have the spec yet, download it:
curl -fsSL -o spec.md "$SPEC_URL"In PowerShell:
curl.exe -fsSL -o spec.md $env:SPEC_URL3. Fetch the delivery
intent-engine deliveries --attestor $JUDGE --intent $INTENTIn PowerShell:
intent-engine deliveries --attestor $env:JUDGE --intent $env:INTENTEach entry in deliveries names the announcing worker, a commitment that binds the work's sha256 to that wallet and a salt, the salt's check, and the signature and slot of its announcement. Add --judge-keypair with the judge's or a committee member's key file to see its url. opened_salt shows a salt that an opening published.
judge pays the earliest announcer whose sealed salt opens its commitment to the work's hash, so a copy sent from another wallet is never paid. Download the work into work, a file or a directory, and treat it as untrusted.
4. Write your check
The check is any command that exits 0 when the work passes the spec. judge gives it three environment variables: SPEC_PATH, the spec file; ARTIFACT_PATH, the work; ARTIFACT_SHA256, the work's hash. This check passes when answer.txt holds 42:
import { readFileSync } from "node:fs";
import { join } from "node:path";
const answer = readFileSync(join(process.env.ARTIFACT_PATH, "answer.txt"), "utf8");
process.exit(answer.trim() === "42" ? 0 : 1);Save it as check.mjs. A bash script works too:
#!/usr/bin/env bash
set -euo pipefail
test "$(cat "$ARTIFACT_PATH/answer.txt")" = 42Save it as check.sh and pass --check check.sh --run "bash check/check.sh" in place of the --check and --run of step 5.
5. Judge and settle
intent-engine judge --owner $OWNER --nonce $NONCE --spec spec.md --artifact work --check check.mjs --run "node check/check.mjs" --verdict verdict.jsonIn PowerShell:
intent-engine judge --owner $env:OWNER --nonce $env:NONCE --spec spec.md --artifact work --check check.mjs --run "node check/check.mjs" --verdict verdict.jsonThis runs your check in a Docker container with no network and sends nothing. Add --yes to run it again and pay the worker on a pass:
intent-engine judge --owner $OWNER --nonce $NONCE --spec spec.md --artifact work --check check.mjs --run "node check/check.mjs" --verdict verdict.json --yesIn PowerShell:
intent-engine judge --owner $env:OWNER --nonce $env:NONCE --spec spec.md --artifact work --check check.mjs --run "node check/check.mjs" --verdict verdict.json --yesThe release records the memo delivery sha256:<artifactSha256>. The payer checks it with intent-engine receipt.
Keep judging
judge --watch takes the same options without --artifact; every --interval seconds (default 30) it downloads each new delivery from its url, checks it against the announcement's commitment and judges it once. It stops at the first pass, which --yes pays, when the escrow closes, or on Ctrl-C. It prints one line of JSON per delivery it judged and one when it stops.
On devnet, testnet and mainnet it refuses a work link that points to a private or local network address.
Judge the word count example
examples/jobs/word-count is a whole job: a spec, a passing delivery and its check. Run these from a clone of this repository. Try the check on the delivery, which prints 32 of 32 inputs hold:
ARTIFACT_PATH=examples/jobs/word-count/delivery/wordcount.mjs node examples/jobs/word-count/check.mjsIn PowerShell:
$env:ARTIFACT_PATH = "examples/jobs/word-count/delivery/wordcount.mjs"
node examples/jobs/word-count/check.mjsAn escrow for this job commits the sha256 that intent-engine hash --artifact examples/jobs/word-count/SPEC.md prints as its specHash. Create one as in step 1, with these values in place of the first two lines there:
export SPEC_URL=https://raw.githubusercontent.com/MerlijnW70/solive/main/examples/jobs/word-count/SPEC.md
export SPEC_HASH=$(intent-engine hash --artifact examples/jobs/word-count/SPEC.md | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).sha256')In PowerShell:
$env:SPEC_URL = "https://raw.githubusercontent.com/MerlijnW70/solive/main/examples/jobs/word-count/SPEC.md"
$env:SPEC_HASH = (intent-engine hash --artifact examples/jobs/word-count/SPEC.md | Out-String | ConvertFrom-Json).sha256Its worker announces the plain sha256 of the file, the hash that intent-engine hash --artifact examples/jobs/word-count/delivery/wordcount.mjs prints. With OWNER and NONCE from step 2, judge and then settle it:
intent-engine judge --owner $OWNER --nonce $NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --check examples/jobs/word-count/check.mjs --run "node check/check.mjs" --verdict verdict.json
intent-engine judge --owner $OWNER --nonce $NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --check examples/jobs/word-count/check.mjs --run "node check/check.mjs" --verdict verdict.json --yesIn PowerShell:
intent-engine judge --owner $env:OWNER --nonce $env:NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --check examples/jobs/word-count/check.mjs --run "node check/check.mjs" --verdict verdict.json
intent-engine judge --owner $env:OWNER --nonce $env:NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --check examples/jobs/word-count/check.mjs --run "node check/check.mjs" --verdict verdict.json --yesJudge without Docker
--sandbox viper judges a delivery that is one JavaScript module, with no Docker and no check of yours. It runs the cases the spec lists in ViperJS, a sandboxed JavaScript engine, and again in a second engine. With OWNER and NONCE from step 2:
intent-engine judge --owner $OWNER --nonce $NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --sandbox viper --verdict verdict.json
intent-engine judge --owner $OWNER --nonce $NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --sandbox viper --verdict verdict.json --yesIn PowerShell:
intent-engine judge --owner $env:OWNER --nonce $env:NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --sandbox viper --verdict verdict.json
intent-engine judge --owner $env:OWNER --nonce $env:NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --sandbox viper --verdict verdict.json --yesAdd --plain-node to also run every case in plain Node inside Docker:
intent-engine judge --owner $OWNER --nonce $NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --sandbox viper --plain-node --verdict verdict.jsonIn PowerShell:
intent-engine judge --owner $env:OWNER --nonce $env:NONCE --spec examples/jobs/word-count/SPEC.md --artifact examples/jobs/word-count/delivery/wordcount.mjs --sandbox viper --plain-node --verdict verdict.jsonSo that a delivery cannot answer from a table of the published cases, the spec's judge-cases block can add "draw": {"count": n, "generator": "<module>", "checker": "<module>", "k": K}. judge then waits until the first block at or after the earliest announcement's slot plus K is finalized, and seeds the draw with the sha256 of a fixed tag, that block's hash, the intent and the work's sha256.
The generator's single export default gets the seed in hex and n and returns n argument lists; the checker's gets one list and the delivery's answer and returns true or false, each in a fresh ViperJS engine with the spec's budgets. judge passes only when every published and drawn case passes, and refuses a spec whose generator or checker fails as the spec's fault.
The verdict's drawn records the slot, blockhash and seed, so anyone with the spec and a node that keeps the full ledger regenerates the same cases.
Who is paid
judge pays the earliest announcement whose sealed salt opens its commitment to the work's hash, so a copied announcement pays nobody else. The salt comes from the worker's own signature, so nobody without the worker's key can test a guessed hash against a commitment. It refuses when nobody announced it.
The release memo records the hash and the payee's salt. After a release, judge sends the salts of every earlier announcement it could open, in order, and - for one it could not, in as many more transactions as the list needs; the verdict's openings shows them. A worker can publish its own salt with the SDK's openDelivery.
Receipts, deliveries, reputation and its trust read a release as valid when the recorded salt opens an announcement by a wallet the release paid, and as refuted when a published salt opens an earlier announcement of the same hash.
This cannot stop a copier who announces every value of a small answer set first, or one who copies work published before its announcement lands. A job claim or a spec with many possible answers covers those.
Check a second node
judge, committee release and mcp take --confirm-url <rpc>, in the forms --url takes, to read the escrow and the earliest announcer from a second RPC node too. A release is signed only when both nodes report the same genesis hash, escrow account and earliest announcer. Otherwise, or when the second node cannot be read, it is refused with ConfirmMismatch and nothing is sent.
intent-engine setup --confirm-url <rpc> checks that the node answers on the same cluster, by genesis hash, then saves it as the confirm-url setting. These three commands use the setting whenever --confirm-url is not given; other commands, such as judge digest, ignore it. --no-confirm-url skips the setting for one run. A second node at the same endpoint as --url is refused. Without a second node, they print once that they read one node.
Judge as a committee
A committee needs a threshold of its members to agree before an escrow pays.
intent-engine committee create --keypair payer.json --members $env:A,$env:B,$env:C --threshold 2 --fee-wallet $env:FEES --yesAn MCP wallet creates one with solive_wallet_committee_create, votes with solive_wallet_committee_vote and releases with solive_wallet_committee_release, all served with --wallet.
The admin can set a committee floor per token with set-committee-floor --mint <mint> --floor <raw>, and show config lists the floors under committeeFloors. The engine refuses a proof escrow of at least its token's floor with CommitteeRequired unless its attestor is a committee with a threshold of two or more. create attested and the escrow tools say so before sending, and pass the committee with the create.
Pass the printed committee address as --attestor when you create the escrow. Each member judges on their own; a pass writes that member's signed vote to the verdict file:
intent-engine judge --keypair a.json --owner $env:OWNER --nonce $env:NONCE --spec SPEC.md --artifact work.mjs --sandbox viper --verdict a.json.verdictA member whose key is on a Ledger votes with committee vote --keypair usb://ledger and the same --owner, --nonce, --pay-to and --result, plus --out for the vote file and --yes. The device shows one message that lists every member's key and the release digest, the digest the preview prints, and signs no transaction. A vote stays valid whichever members reach the threshold.
A member on a Ledger can also judge, with judge --keypair usb://ledger --fee-payer <key file> --encryption-keypair <file>. It opens the announcements with the encryption key its profile publishes and, with --yes, signs its vote on the device; without --yes it prints the vote's digest and signs nothing. Without an encryption key file it refuses announced work.
Because that message lists every member, a release by a large committee, especially one that also splits the pay, can exceed the size of one Solana transaction; keep committees small or the escrow can only be cancelled.
A member's verdict also carries the payee's salt and the earlier salts it opened. committee release refuses votes with different payee salts, records the salt and, with --yes, sends the earlier salts any member opened after the release. A failed opening is reported beside the release signature.
Any member who voted then submits the votes:
intent-engine committee release --keypair a.json --vote a.json.verdict --vote b.json.verdict --sha256 $env:WORK --yesJob claims
With claims, a worker holds a job for a while against a stake, and only that worker can be paid while the claim holds. The owner opens claims:
intent-engine claims open --keypair owner.json --nonce $env:NONCE --max-secs 7200 --stake 0.05 --yesA worker reads the board and takes the job:
intent-engine claims show --owner $env:OWNER --nonce $env:NONCE
intent-engine claims take --keypair worker.json --owner $env:OWNER --nonce $env:NONCE --secs 3600 --yesA worker that drops its claim gets its stake back. A claim that lapses loses its stake to the owner.
While a job is held, another worker can stand by in its first-in, first-out line, locking the same stake. While anyone stands by, nobody claims the job directly, and a drop hands it to the first worker still in the line.
Once a claim lapses, anyone can run claims advance, which forfeits the lapsed stake to the owner and hands the job on. A waiter takes back its stake and rent with claims leave, and anyone can do that for it once the escrow is no longer open. jobs counts the waiters as waiting.
intent-engine claims standby --keypair worker.json --owner $env:OWNER --nonce $env:NONCE --secs 3600 --yes