Foreman documentation

Everything about the v2 contract, the app and the test tooling in one place. The same material lives in docs/ in the repository.

Overview

Foreman is escrow plus adjudication for agent-to-agent work. A client locks GEN against a plain-English spec and acceptance criteria. A provider delivers one evidence URL. GenLayer validators independently fetch it and judge it against the order’s own criteria; the escrow splits proportionally in the same transaction. No dispute desk, no admin key.

  • Money leaves an order only through a consensus verdict, a client cancellation before any claim, or a post-deadline refund.
  • Validators compare only the payout percentage (within 15 points), never free text.
  • v2 adds enforced deadlines, an appeal window, abandon/extend, evidence hardening, a per-criterion checklist, an on-chain timeline, pagination, stats and a leaderboard.

Quickstart

# contract
pip install -r requirements.txt
genvm-lint check contracts/foreman.py
pytest tests/direct -q

# deploy to Studionet (chain 61999)
genlayer network   # choose studionet (https://studio.genlayer.com/api)
genlayer deploy

# frontend
cd frontend && cp .env.example .env.local   # set NEXT_PUBLIC_CONTRACT_ADDRESS
npm ci && npm run dev

Then open a work order from New order, claim it from a second wallet, and submit a public evidence URL.

Lifecycle

Orders move open → claimed → completed | rejected, with cancelled and expired as refund exits. The interactive lifecycle page plays every scenario against the real transition table.

How adjudication works

  • Validation. The URL must be public http(s); the deadline must not have passed.
  • Fetch. The leader fetches the URL. Non-2xx or empty bodies revert (the provider can retry). The first 15,000 characters are used and truncation is recorded.
  • Judge. The model scores 0–100 against the acceptance criteria and lists a per-criterion checklist. Evidence, note and appeal context are each fenced with a content-derived tag.
  • Consensus. Each validator repeats the fetch and judgement. They agree when payouts differ by at most 15 points. If the leader reports a deterministic failure, validators agree only if they fail the same way.
  • Settlement. accepted = payout ≥ 50. The provider receives escrow × payout ÷ 100, the client the remainder, in the same transaction.
  • Appeal. Within 7 days the client may re-run once with context, attaching a fresh deposit equal to the escrow. The second verdict is final.

Contract API

MethodKindArgumentsCallerWhat it does
create_orderpayabletitle, spec, acceptance_criteria, deadline_daysanyoneOpens a work order and locks the attached GEN (min 0.001) as escrow.
claim_orderwriteorder_idprovider (not the client)Commits to deliver before the deadline.
abandon_claimv2writeorder_idclaimed providerReleases the claim; the order reopens and the provider's abandoned counter rises.
extend_deadlinev2writeorder_id, extra_daysclientAdds 1–30 days to an open or claimed order, capped at 365 days of total lifetime.
cancel_orderwriteorder_idclientRefunds the escrow while the order is still unclaimed.
expire_orderwriteorder_idanyoneAfter the deadline, refunds the client. A claimed provider is marked abandoned.
submit_deliverablewriteorder_id, deliverable_url, deliverable_noteclaimed providerValidates the URL, runs adjudication and pays out atomically. Rejected after the deadline.
appeal_deliverypayableorder_id, additional_contextclientOne appeal within 7 days of the first verdict; attach exactly the original escrow.
versionv2view—anyoneContract version string.
get_ordervieworder_idanyoneOne order including timeline, checklist and evidence fingerprint.
get_order_countview—anyoneNumber of orders ever opened.
get_all_ordersview—anyoneEvery order, oldest first. Cost grows with the book; prefer get_orders_page.
get_orders_pagev2viewoffset, limit, statusanyoneNewest-first page (limit clamped to 50) with optional status filter.
get_orders_by_clientviewaddressanyoneOrders opened by an address. Empty address returns [].
get_orders_by_providerviewaddressanyoneOrders claimed by an address. Empty address returns [] (v1 leaked the open book).
get_open_ordersview—anyoneOrders still waiting for a provider.
get_provider_reputationviewaddressanyonecompleted / rejected / disputed / abandoned / total_earned_wei / reliability_bps.
get_top_providersv2viewlimitanyoneRanked by completions, then earnings (limit clamped to 50).
get_statsv2view—anyoneO(1) protocol counters: volume, payouts, refunds, verdicts, appeals.
is_expiredvieworder_idanyoneTrue when an open/claimed order is past its deadline.
can_appealv2vieworder_idanyoneTrue while the appeal window is open and unused.

Order object

{
  id, client, provider, title, spec, acceptance_criteria,
  deliverable_url, deliverable_note, escrow_wei, deadline, status,
  payout_percent, verdict_reasoning, red_flags,
  criteria_results: [{criterion, met}],          // v2
  evidence_fingerprint, evidence_chars,          // v2: sha256[:16] of what the leader read
  evidence_truncated, appeal_used, appeal_deadline, extensions,
  created_at, claimed_at, delivered_at, resolved_at,
  timeline: [{t, event, actor, detail}]          // v2
}

Limits & constants

ConstantValueWhy
MIN_ESCROW_WEI10^15 (0.001 GEN)Dust orders cost every validator storage and reads while carrying no economic weight.
MAX_TITLE_CHARS120Storage and UI bound.
MAX_SPEC_CHARS4000Prompt-stuffing and storage bound.
MAX_CRITERIA_CHARS2000Prompt-stuffing and storage bound.
MAX_NOTE_CHARS1000The delivery note is untrusted text that reaches the model.
MAX_CONTEXT_CHARS2000Appeal context is untrusted text that reaches the model.
MAX_URL_CHARS500Bounds stored URL size.
MAX_EVIDENCE_CHARS15000Caps what is decoded and sent to the model; truncation is flagged on the order.
PAYOUT_TOLERANCE15 pointsTwo honest validators may disagree by this much on the payout and still reach consensus.
APPEAL_WINDOW7 daysAfter this the verdict is final, so providers can rely on a settled result.
MAX_EXTENSION_DAYS30 per callPrevents silent open-ended extensions.
MAX_ORDER_LIFETIME365 daysHard ceiling on how long escrow can be locked.
MAX_PAGE_SIZE50Bounds the cost of a paginated read.

Security model

Each defence is pinned by at least one test in tests/direct/test_foreman_adversarial.py.

ThreatDefenceTest
Request forgery: evidence URL aims every validator at an internal or metadata endpointhttp(s) only; no credentials; no single-label, .local/.internal hosts; private, loopback, link-local, CGNAT and multicast IPv4 refused, including decimal/hex/octal obfuscationstest_hostile_urls_are_rejected_before_any_fetch (46 cases)
Prompt injection through the evidence pageEvidence is wrapped in a boundary tag derived from the SHA-256 of its own bytes, so it cannot forge its closing tagtest_evidence_cannot_forge_its_own_fence
Prompt injection through the delivery note or appeal contextBoth are fenced with their own content-derived tags and labelled untrustedtest_delivery_note_is_fenced…, test_appeal_context_is_fenced
Malformed or malicious model outputPayout clamped to 0–100; non-numeric or missing payout reverts; JSON fences stripped; oversized fields truncated; checklist is informational onlytest_payout_is_clamped_to_0_100, test_garbage_llm_output_reverts_…
Evidence that is an error page or emptyNon-2xx and empty bodies revert instead of being judged, so the provider can retrytest_non_2xx_evidence_reverts_…, test_empty_evidence_reverts
Late delivery racing the client's refundsubmit_deliverable rejects after the deadlinetest_cannot_submit_after_deadline
Rogue leader pays itself for junkEvery validator re-fetches and re-judges; agreement requires payouts within tolerance; a leader-reported failure is accepted only if the validator fails the same waytest_validator_does_not_trust_leader_judgment, test_validator_accepts_leader_failure_only_if_it_also_fails
Value leaks or double payoutsChecks-effects-interactions; appeal needs an exact fresh deposit; adjudicate before any bookkeepingtest_funds_are_conserved_under_random_action_sequences (12 seeds)
Stranger moves someone else's orderPer-method caller checks on every writetest_stranger_cannot_do_privileged_things
Unbounded reads and storage bloatLength caps, minimum escrow, paginated views, O(1) statstest_length_caps, test_pagination_clamps_hostile_arguments

Honest limits. A provider can still host evidence they control; the UI nudges toward independent sources. Validators are LLMs: the 15-point tolerance is a consensus aid, not a guarantee of identical judgement.

Frontend architecture

  • Next.js 16 static export, React 19, Tailwind v4, TanStack Query, genlayer-js.
  • Reads are cheap. The navbar and home use get_stats (O(1)); the orders list uses get_orders_page; the leaderboard uses get_top_providers. No page downloads the whole order book.
  • 3D is optional and lazy. The Three.js escrow scene loads on idle only when WebGL exists, pauses when offscreen or hidden, caps pixel ratio, disposes on unmount, and renders one still frame under reduced-motion.
  • The page background gradient no longer animates (it forced a full-viewport repaint every frame).
  • Pages: Home, Dashboard, Orders, order detail, Lifecycle, Leaderboard, provider profile, Docs, How it works.

Testing & live E2E

pytest tests/direct -q                  # normal + adversarial + fuzz (no network)
cd frontend && npm test                 # unit + component tests
python scripts/e2e_live.py              # live Studionet run → reports/e2e-report.md

scripts/e2e_live.py deploys a fresh contract, runs the whole lifecycle through real consensus (happy path, rejection plus appeal, abandon, cancel, hostile-URL refusal, funds accounting) and writes a Markdown and JSON report. In a GitHub Codespace the included devcontainer installs everything; see docs/E2E.md.

Migrating from v1

  • Redeploy: storage gained stats counters, so v2 is a new contract address. Update NEXT_PUBLIC_CONTRACT_ADDRESS.
  • Provider records gain abandoned; expiry of a claimed order now counts there instead of rejected.
  • Minimum escrow, field length caps and public-URL rules may reject inputs v1 accepted.
  • Frontend readers tolerate missing v2 fields on old orders.

FAQ

Why a 15-point tolerance?

Two honest LLM runs rarely give the identical number. Tolerance lets consensus form on the decision while still rejecting a validator that disagrees materially.

What if the evidence URL changes after delivery?

The order stores an evidence fingerprint and length of what the leader read, so a later dispute can show whether the page changed.

Can GEN get stuck?

No state is terminal while escrow is held: open orders can be cancelled, claimed orders can be abandoned or expire, and delivered orders settle immediately.