Skip to content

About

Escrowed freelance delivery settlement judged by GenLayer validators against locked, commit-pinned documents.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation


GenLayer Python Network Tests License Status CI



ServicePact

Trustless freelance work-delivery settlement — powered by GenLayer AI-validator consensus

A client escrows funds on-chain. A contractor delivers. GenLayer validators independently
fetch every commit-pinned document, adjudicate six frozen criteria, and settle the payment
— without any trusted third party, arbitrator, or human override.


Overview · Flow · Architecture · Security · Quick Start · Testing · Deployment · Docs




Overview

Freelance payment disputes happen when a client and contractor disagree on whether delivery meets the agreed scope, quality, and timeline. Today those disputes are resolved by platforms, escrow services, or courts: trusted third parties that add cost, delay, and counterparty risk.

ServicePact replaces the third party with GenLayer's decentralised AI-validator consensus. The statement of work, the acceptance checklist, the six criteria and the deadline are frozen on-chain before a single wei is escrowed. When the contractor submits a delivery, validators independently fetch every commit-pinned document, judge it against the frozen criteria, and the contract derives the settlement from criterion statuses. The model names facts, not payment amounts. Settlement logic is code.

Who benefits from a false verdict: the contractor from a false DELIVERED, the client from a false REJECTED. That is why the judgment runs under validator consensus.


Property Value
Contract language Python (GenVM Intelligent Contract, stable v0.2 API)
Runner hash 1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6
Consensus model GenLayer Optimistic Democracy
Equivalence strategy Exact agreement on every validator-judged criterion status
Criteria count 6 (frozen at creation): 3 computed by code, 3 judged by validators
Repair budget Configurable (0 to 3 revisions), 7-day repair window
Direct-mode tests 149
Network StudioNet (gasless, temporary)
Concept Escrowed freelance delivery settlement against locked documents
Evidence source Commit-pinned text files on raw.githubusercontent.com, fetched by validators

Evidence sources (one row per source):

Host URL pattern Fetch method Deciding field
raw.githubusercontent.com /<owner>/<repo>/<40-hex commit>/<path>, derived by the contract from (repo, commit, path); no caller URL is ever accepted web.get text of each SOW, checklist and work-product file

A commit-pinned path cannot change after it is committed to, so content cannot be swapped between commitment and judgment.



Status

Tested Deployed Live Payout Reclaim Untested

Oct 23 2026 steward fixes. The steward asked for (1) a neutral recovery path when an engagement stays SUBMITTED because adjudication does not finalize, (2) client-controlled SOW or checklist evidence that cannot be withheld for profit, and (3) lossless amounts. They are implemented in contracts/service_pact.py and covered by direct-mode tests (see Steward fixes below). The corrected contract is deployed to StudioNet at 0x00F2AB76B6345CAA7EEbCB46dcCEC447d91B1370 and CONTRACT_ADDRESS points to it. None of the three new paths has been exercised on a live network yet; the live runs described below were made on the previous deployment.

Previous deployment (before the fixes): StudioNet 0xDFC205f0AB2D2377A898D84A5360A2Baa661F885; the explorer shows the constructor transaction finalized with GenVM result SUCCESS. Transaction links for the observed runs are in docs/deployment.md. On Oct 21 2026 the in-app guide drove live runs on it: three adjudications reached validator consensus (explorer: GenVM SUCCESS, consensus Accepted). 149 tests pass with pytest tests -q -p no:cacheprovider, genvm-lint check passes, and the frontend type-checks and builds. The tests execute the real contract with the web and the model mocked; they do not prove real LLM behaviour, real network behaviour, real multi-node consensus timing, or the deadline and repair-window exits on a live network. StudioNet is a temporary test network: deployed addresses may not survive resets, so the repository tests are the proof of behaviour, not an address.



Flow


┌─────────────────────────────────────────────────────────────────────────────┐
│                          ServicePact Lifecycle                              │
└─────────────────────────────────────────────────────────────────────────────┘

  CLIENT                           CHAIN                         CONTRACTOR
    │                                │                                │
    │──── create_engagement() ──────▶│  Lock SOW + criteria + amount  │
    │                                │  State: DRAFT                  │
    │──── fund_engagement()  ───────▶│  Escrow exact amount           │
    │                                │  State: FUNDED                 │
    │──── open_engagement()  ───────▶│  State: OPEN                   │
    │                                │                                │
    │                                │◀─── submit_delivery() ─────────│
    │                                │  Lock work-product (pinned)    │
    │                                │  State: SUBMITTED              │
    │                                │                                │
    │                            adjudicate_engagement()              │
    │                         (callable by anyone)                    │
    │                                │                                │
    │                    ┌───────────┴────────────┐                   │
    │                    │  AI-Validator Consensus │                   │
    │                    │  ┌─────────────────┐   │                   │
    │                    │  │ Leader fetches  │   │                   │
    │                    │  │ all documents   │   │                   │
    │                    │  │ calls LLM       │   │                   │
    │                    │  └────────┬────────┘   │                   │
    │                    │           │             │                   │
    │                    │  ┌────────▼────────┐   │                   │
    │                    │  │ Validators each │   │                   │
    │                    │  │ re-fetch + re-  │   │                   │
    │                    │  │ run LLM         │   │                   │
    │                    │  │ compare statuses│   │                   │
    │                    │  └────────┬────────┘   │                   │
    │                    └───────────┼────────────┘                   │
    │                                │                                │
    │          ┌─────────────────────┼──────────────────┐            │
    │          │                     │                  │            │
    │      DELIVERED              REJECTED           STALLED         │
    │          │                     │                  │            │
    │          │               CLIENT reclaims    CONTRACTOR         │
    │          │               full escrow        resubmits          │
    │          │                     │            (if budget left)   │
    │          │               RECLAIMED               │             │
    │          │                                   ────┘             │
    │          │                                (loops back to       │
    │          │                                 SUBMITTED)          │
    │          │                                                      │
    │          │◀──────── release_payment() ─────────────────────────│
    │          │          Contractor receives                         │
    │          │          exact escrowed amount                       │
    │          │          State: RELEASED                             │


Contract Architecture

State machine


DRAFT ──fund──▶ FUNDED ──open──▶ OPEN ──submit──▶ SUBMITTED
                                                       │
                                             adjudicate_engagement()
                                                       │
                              ┌────────────────────────┼────────────────────────┐
                              ▼                        ▼                        ▼
                          DELIVERED               REJECTED                  STALLED
                              │                        │                        │
                    release_payment()          reclaim_funds()          resubmit or
                              │                        │                reclaim (exhausted)
                              ▼                        ▼
                          RELEASED               RECLAIMED

Other exits: a SUBMITTED engagement that is still not adjudicated 3 days after the delivery was recorded can be recovered by anyone with recover_unadjudicated (no-fault 50/50 split, see Steward fixes); an EVIDENCE_HELD engagement is described there too; a FUNDED engagement can be reclaimed by the client at any time; an OPEN engagement with no delivery can be reclaimed once the deadline has passed; a STALLED engagement can be reclaimed once the repair budget is used or the 7-day repair window ends.

Storage records

Two on-chain records make up the full engagement:

Engagement: created by create_engagement. The policy fields (sow_json, checklist_json, criteria_json, deadline_iso, deadline_epoch, max_repairs, amount, client, contractor) are permanently frozen. Only state, revision, first_submitted_at, stalled_at, adjudication_json and the receipts change.

Packet: created by submit_delivery or resubmit_delivery, keyed <engagement_id>#<revision>. Each revision is a new record; once a packet reaches a terminal state it is never overwritten. A paged index (engagement_order) backs the list view, so reads stay bounded.

Six frozen acceptance criteria


Criterion ID What it checks Decided by
engagement_identity Work product contains engagement:<id> code
contractor_binding Work product contains contractor:<address> code
scope_completeness Work product covers every deliverable in the SOW validators
quality_standard Evidence meets the frozen quality requirement validators
timeline_compliance First submission time is within the locked deadline code
acceptance_checklist Work product satisfies every item of the client's checklist validators

All six must be SATISFIED for a DELIVERED verdict. Any single VIOLATED criterion yields REJECTED. If the contractor's work was fetched and bound but a client-controlled document (SOW or checklist) is unavailable, the outcome is EVIDENCE_HELD, then CLIENT_DEFAULT (never a client refund). Any other UNRESOLVED criterion (unavailable work document, missing marker, ambiguous evidence, malformed model reply) yields STALLED. A late first delivery is VIOLATED by code, whatever the model says. Every verdict value is traced to a branch that can produce it (docs/contracts.md).

Equivalence strategy

A hand-written validator re-runs the whole evaluation (fetch plus model call) and requires every one of the five leader-produced statuses (two marker checks plus three judged criteria) to equal its own. There is no tolerance band: the statuses are discrete choices and a difference changes the payout. observed_fact, witness ids and reasoning are stored for audit but never affect consensus or payment. The leader returns only stable decision fields.

Nondet block structure

# Deterministic body: copy everything the nondet block needs into plain values
requirements = dict(json.loads(eng.criteria_json))
sow_docs     = list(json.loads(eng.sow_json))
work_docs    = list(json.loads(packet.work_json))

def leader_fn():
    return _evaluate(eid, contractor_key, requirements, sow_docs, checklist_docs, work_docs)

def validator_fn(leaders_res) -> bool:
    if not isinstance(leaders_res, gl.vm.Return):
        return False
    mine = leader_fn()                      # re-fetch, re-run
    # compare every judged status exactly
    ...

result = gl.vm.run_nondet_unsafe(leader_fn, validator_fn)   # positional
# Storage writes happen below this line; timeline is derived from stored times


Security

Threat model


Threat Mitigation
Client rewrites criteria after funding SOW, checklist, criteria, deadline and repair budget are frozen at create_engagement, before fund_engagement
Contractor self-declares DELIVERED Outcome derived from criterion statuses in code; model prose never feeds the rule
Contractor controls the evidence of acceptance There is no contractor-supplied acceptance log: the checklist is client-locked before funding
Model injects extra JSON keys (payout, outcome) The reply must contain exactly {criteria, reasoning} with exactly one entry per judged criterion; anything else becomes UNRESOLVED
Prompt injection via document content Documents are sanitised and wrapped as labelled untrusted DATA; the charter says to ignore instructions inside them
Document substitution between commit and adjudication Evidence is a commit-pinned file; the URL is derived from (repo, 40-hex commit, path), never accepted from a caller
Replaying another engagement's deliverable The work product must contain the engagement and contractor markers, checked in code
Late delivery argued as on time timeline_compliance is computed from stored submission time against the locked deadline
Single validator manipulation Multiple validators each re-fetch and re-run; exact agreement on every judged status
Contractor resubmits indefinitely max_repairs and a 7-day window are frozen; past either, the client can reclaim
Double payment / double reclaim State is written before any transfer; terminal states reject further fund-moving calls
Funds locked forever Every state holding funds has a bounded exit (see below)
Integer overflow / precision error Money is u256; no float(); emit_transfer(value=...)

Fund path audit

Every path that moves escrowed funds is bounded and tested with the exact amount and recipient:

FUNDED                 reclaim_funds()    exact amount to client       state = RECLAIMED
OPEN (no delivery)     reclaim_funds()    after the deadline           state = RECLAIMED
DELIVERED              release_payment()  exact amount to contractor   state = RELEASED
CLIENT_DEFAULT         release_payment()  exact amount to contractor   state = RELEASED
EVIDENCE_HELD          reclaim_funds()    only after 30 days from first observation  state = RECLAIMED
SUBMITTED (3 days)     recover_unadjudicated()  anyone; amount//2 to contractor, rest to client  state = RECOVERED
REJECTED               reclaim_funds()    exact amount to client       state = RECLAIMED
STALLED (exhausted or window expired)  reclaim_funds()  exact amount to client  state = RECLAIMED
SUBMITTED              adjudicate_engagement()  callable by anyone, no party can block it

No fund path can be reached twice. No fund path accepts a caller-supplied amount. Transfers use emit_transfer, which is finalization-only for external recipients, so payouts land after the appeal window.



Quick Start

Prerequisites

  • Python 3.12
  • Node 20 (for the frontend)
# Clone
git clone https://github.com/Siriron/servicepact
cd servicepact

# Contract tests (no chain, no Docker, no LLM key)
pip install "genlayer-test==0.29.2" genvm-linter
genvm-lint download --version v0.2.16
genvm-lint check contracts/service_pact.py
pytest tests -q -p no:cacheprovider

# Frontend
npm install
npm run dev

Expected output of the tests:

100 passed in Xs

The tests pin GenVM v0.2.16 (sdk_version in every deploy). Unpinned, genlayer-test resolves GitHub's latest GenVM release, which can be a release candidate whose bundle is not published.



Steward fixes (Oct 23 2026)

1. Neutral recovery when adjudication does not finalize. If an engagement stays SUBMITTED for 3 days after its delivery was recorded, anyone may call recover_unadjudicated(engagement_id). The escrow is split without reading any document, model reply or caller input: floor(amount / 2) to the contractor, the remainder to the client; state RECOVERED. It is only reachable from SUBMITTED, only once, and the clock restarts on every resubmission. Anyone can still adjudicate first, and an adjudication that finalizes wins. Deliberate gap: a party who expects an unfavourable verdict may prefer the split, which is why the timeout is long and adjudication is open to all.

2. Client-controlled evidence cannot be withheld for profit. The contractor controls the work documents, the client controls the SOW and checklist. Unavailable work documents still stall (repair, then the client may reclaim). Unavailable SOW or checklist documents no longer stall in the client's favour: when the work was fetched and both markers bind, the result is EVIDENCE_HELD and the first observation time is stored. The client cannot reclaim. A later adjudication (anyone) that still cannot fetch them after 3 days gives CLIENT_DEFAULT, and the contractor releases the full escrow. Restoring the documents resumes normal judgment and resets the clock. A late first delivery is still REJECTED, missing markers or work documents still STALLED. If nobody ever triggers the second observation, the client may reclaim after 30 days. Validators compare the exact list of unavailable client documents, not only the statuses. Deliberate gap: a transient outage must be seen twice, 3 days apart, before it can pay the contractor.

3. Lossless amounts. Amounts up to 10^24 wei exceed JavaScript's 2^53 safe integer range. get_engagement returns amount, client_receipt and contractor_receipt as decimal strings, release_payment, reclaim_funds and recover_unadjudicated return amounts as strings, and the frontend keeps them as strings and BigInt (create, fund, display); the form rejects anything outside 1 to 10^24.

The in-app test guide has new scenarios for the withheld SOW and for the recovery path (both need a 3-day wait on a live network; the guide shows when the wait ends).

Testing

Test suite: 149 direct-mode tests that execute the real contract


Group What it proves
create_engagement validation Duplicate ids, bad amount, bad deadline, missing criteria, bad address, same client and contractor, and every manifest shape that is not an identifier-derived commit-pinned document
Fund and open Exact value required; only the client may fund and open; no double funding
submit_delivery Only the contractor, only once, only when OPEN; a work document cannot shadow a client document id
Every verdict value DELIVERED, REJECTED (each judged criterion), STALLED (each judged criterion); VIOLATED beats UNRESOLVED
Code-computed criteria Late first delivery is rejected even if the model is happy; missing engagement or contractor marker stalls; another engagement's deliverable does not bind
Evidence handling HTTP 403/404/500 and unavailable client documents become a failure marker, never evidence; documents reach the model wrapped as untrusted data
Malformed model output Non-JSON, wrong shape, bad status, unknown witness, duplicate criterion, empty reasoning: all stall, nothing pays out
Validator agreement Agrees on a match; rejects a difference in any single judged criterion; rejects different binding evidence, unreachable evidence, an errored leader, a malformed leader result
Funds Exact amount and recipient for release and reclaim; no double payout; access control by sender
Bounded exits Funded and never opened; opened and never delivered; budget exhaustion; repair-window expiry
Multi-step flows Four-revision repair flow ending in payment; first submission time kept across repairs
Replay and rollback snapshot/revert; independent engagements; storage-pickling check
Recovery path Split after the timeout only, exactly once, by anyone; rounding never creates or loses a wei (amounts 1, 2, 1001, 2^53+1, 10^24); impossible from any other state; clock restarts on every resubmission; adjudicating first wins
Withheld client evidence SOW or checklist (or both) missing: EVIDENCE_HELD, client cannot reclaim, no recovery split; second observation after 3 days gives CLIENT_DEFAULT and the contractor is paid in full; restoring the documents resumes judgment and resets the clock; late delivery, missing markers, or missing work documents never default; client abandonment exit after 30 days; validator rejects a different unavailable set
Lossless amounts Every permitted amount (2^53-1, 2^53, 2^53+1, 10^18+1, 123456789012345678901234, 10^24) is a JSON string in views and write results, exact through create, fund, release, reclaim, recovery and default; 0 and 10^24+1 rejected
Time helper Calendar oracle cases including 2100 not being a leap year; malformed input returns 0

Planted bugs (the status_code mistake, a validator that skips agreement, a missing timeline check, a century leap-year bug, a wrong payout recipient, a removed STALLED exit, amounts returned as numbers, a removed client-evidence hold, a missing recovery timeout, a validator that ignores the unavailable-document list, odd wei assigned to the wrong side) each fail the suite. The tests mock the web and the model; they do not prove real LLM behaviour, real network behaviour or real multi-node timing.

Running specific groups

# All tests
pytest tests -q -p no:cacheprovider

# One test
pytest tests -v -k "test_delivered_when_everything_is_satisfied" -p no:cacheprovider

# Fund-path and exit tests only
pytest tests -v -k "release or reclaim or exit or stalled" -p no:cacheprovider


Deployment

The current contract is deployed at 0x00F2AB76B6345CAA7EEbCB46dcCEC447d91B1370 and CONTRACT_ADDRESS in src/config/chains.ts points to it. To redeploy, deploy contracts/service_pact.py and follow the steps below and update that constant.

1. Lint the contract

pip install genvm-linter
genvm-lint check contracts/service_pact.py

2. Deploy to StudioNet (gasless)

Deploy contracts/service_pact.py from studio.genlayer.com (Run and Debug), or with the GenLayer CLI:

npm install -g genlayer
genlayer network set studionet      # Chain ID 61999, RPC https://studio.genlayer.com/api
genlayer deploy --contract contracts/service_pact.py
genlayer schema <address>

3. Create your first engagement

Manifests are JSON arrays of {document_id, repo, commit, path}. The commit must be a full 40-character SHA.

genlayer write <address> create_engagement --args '[
  "eng-001",
  "0xContractorAddress",
  1000000,
  "[{\"document_id\":\"sow1\",\"repo\":\"owner/spec\",\"commit\":\"<40-hex>\",\"path\":\"sow.md\"}]",
  "[{\"document_id\":\"check1\",\"repo\":\"owner/spec\",\"commit\":\"<40-hex>\",\"path\":\"checklist.md\"}]",
  "{\"engagement_identity\":\"...\",\"contractor_binding\":\"...\",\"scope_completeness\":\"...\",\"quality_standard\":\"...\",\"timeline_compliance\":\"...\",\"acceptance_checklist\":\"...\"}",
  "2030-01-01T00:00:00Z",
  2
]'

The in-app Test guide (/guide) runs six scenarios end to end with pre-filled forms and a copyable transaction log, and is the easier route.

4. Debug a failed transaction

genlayer receipt <txHash> --stdout --stderr
genlayer schema <address>
genlayer call <address> get_engagement --args '["eng-001"]'

5. Deploy the frontend

npm run build, then deploy dist to Vercel (vercel.json already rewrites to index.html).



Project structure

contracts/service_pact.py            The GenVM contract
tests/test_direct.py                 Direct-mode tests that execute the real contract
pytest.ini                           Makes unused test mocks fail the suite
evidence/fixtures/                   Public documents used by the in-app test guide
scripts/check_frontend_vs_schema.py  Checks frontend calls against the real ABI
.github/workflows/ci.yml             Lint + tests + build on every push
src/                                 React + Vite app (includes /guide and /docs routes)
docs/                                architecture.md, deployment.md, contracts.md, frontend.md
LICENSE                              MIT


Docs


Document Description
docs/architecture.md Boundary, evidence sources, criteria, validator rule, state machine
docs/contracts.md Method table, bounded exits, verdict reachability, deliberate gaps
docs/deployment.md Deployment steps and exactly what the tests do and do not prove
docs/frontend.md Client setup, receipt handling, routes and the test guide


Playbook compliance


Rule Status
Pinned runner hash on line 1 (1jb45…, stable v0.2 API) ✅
@allow_storage + @dataclass for all structs ✅
TreeMap for key-value storage; no DynArray construction ✅
Views and writes return json.dumps() strings; zero float() ✅
run_nondet_unsafe(leader_fn, validator_fn) positional; both nested; no self. inside ✅
leader_fn returns a parsed dict; validator_fn checks isinstance(…, gl.vm.Return) first ✅
Nondet block touches no storage; inputs copied to plain values first ✅
Module-level constants only; no type-annotated class-body constants ✅
emit_transfer(value=) for value moves; never .send() ✅
No status_code; HTTP response uses .status ✅
Timestamps parsed from the ISO string with integer arithmetic ✅
Evidence fetched contract-side from identifier-derived URLs; untrusted input sanitised and wrapped ✅
Every verdict value traced to a branch that can produce it ✅
All locked fields frozen before funding ✅
Bounded exit on every fund path ✅
genvm-lint check passes ✅
149 direct tests with the SDK pinned to v0.2.16; planted bugs fail them ✅
Steward fixes (recovery path, withheld client evidence, lossless amounts) ✅ tests, ✅ deployed to StudioNet, ⏳ not observed live
Live StudioNet deployment (current contract) ✅ deployed at 0x00F2AB76B6345CAA7EEbCB46dcCEC447d91B1370, no transactions observed yet
Live adjudication consensus (previous contract only) ✅ observed (STALLED, repair, DELIVERED; marker replay guard)
Live release_payment (previous contract only) ✅ finalized, returned released: 1, state RELEASED (recipient balance not checked)
Live reclaim_funds after repair-budget exhaustion (previous contract only) ✅ STALLED (revision 2 of 2) then RECLAIMED, 1 returned to the client
Live deadline and repair-window exits ⏳ not yet observed (tests only)


License

MIT — see LICENSE


Built with GenLayer · AI-validator consensus · trustless settlement

About

Escrowed freelance delivery settlement judged by GenLayer validators against locked, commit-pinned documents.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages