When a shared pot has to be divided and "fair" is a judgment call, FairSplit escrows the money on GenLayer and lets a decentralized jury of language models rule on the split. No single person holds the funds or makes the decision.
- Live app: https://fairsplit-beta.vercel.app/
- Contract (studionet):
0x905a9dd3f9bD8639BB4B420fA252564B53A02a57(runs the reviewer-requestedrun_nondet_unsafe+ customvalidator_fn; previous0x6eF9Bb75…d21066retired.) - Explorer submission draft:
deliverables/SUBMISSION.md
- Why this dies without GenLayer
- The problem
- How FairSplit works
- Architecture
- Contract API
- Deploy the contract (studionet)
- Run the frontend locally
- Deploy the frontend on Vercel
- Run the tests
- How consensus actually works here
- FAQ
- Notes & limits
The core is a subjective judgment over unstructured evidence with real money escrowed. A Solidity contract cannot read two conflicting human narratives, weigh their evidence, and decide a fair division. GenLayer's Intelligent Contracts can — the LLM jury reasons on-chain and validators reach consensus on the meaning of the ruling, not its byte-for-byte format. Remove the AI and there is no product, only a manual mediator holding other people's money.
Roommates ending a lease, friends who co-bought equipment, a small partnership winding down, a group trip's shared kitty — dividing shared money or debt on a split-up is one of the most common sources of interpersonal conflict. "Fair" is subjective: it depends on who contributed more, who used more, and whose account of events is more credible. Existing options are all bad:
- Do it yourselves → deadlock, because each side is biased toward itself.
- Escrow with a friend → one person holds the money and makes the call; the loser never trusts the outcome.
- Hire a mediator / lawyer → fees eat the pot for small amounts.
- A normal smart contract → can only split by a hard-coded formula. It cannot read arguments and judge fairness.
- Party A opens a case, escrows a deposit, and states their argument (plus an optional evidence link — receipts, a shared doc, a chat export).
- Party B joins, escrows their deposit, and states their side.
- The AI jury rules. The contract reads both arguments, optionally fetches
each side's evidence URL on-chain via
gl.nondet.web.render, and asks an LLM to decide what percentage of the pot each party fairly deserves. Validators independently re-judge and reach consensus on the split. - Either party may appeal once, which re-runs the arbiter.
- Finalize releases the escrowed pot to both parties in the awarded proportions — trustlessly, straight from the contract.
┌────────────────┐ sign tx ┌────────────┐ RPC ┌────────────────┐
│ User's browser │ ───────────► │ MetaMask │ ──────────► │ genlayer-js │
│ (Vite/React) │ │ chain 61999│ │ createClient() │
└────────────────┘ └────────────┘ └───────┬────────┘
│ writeContract
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ FairSplit Intelligent Contract │
│ (contracts/fair_split.py) │
│ │
│ storage: cases, parties_a, parties_b ← TreeMap[str, dataclass] │
│ │
│ _arbitrate(case, a, b): │
│ leader_fn(): │
│ web = gl.nondet.web.render(evidence_url) ← on-chain fetch │
│ json = gl.nondet.exec_prompt(prompt) ← on-chain LLM │
│ return { creator_bps, counter_bps, rationale } │
│ gl.vm.run_nondet_unsafe(leader_fn, validator_fn) │
│ validator_fn: re-runs leader_fn, agrees iff splits are within │
│ 10 percentage points ← MEANING, not JSON bytes │
└──────────────────────────────────────────────────────────────────────────┘
Nothing off-chain touches the ruling. There is no arbitration server, no oracle, no signed message from the frontend that says "here is the answer".
| Method | Kind | Purpose |
|---|---|---|
create_case(title, argument, evidence_url, counterparty) (payable) |
write | Open a case; escrow the creator's deposit. Returns the case id. |
join_case(case_id, argument, evidence_url) (payable) |
write | Counterparty joins and escrows their deposit. |
request_ruling(case_id) |
write | Run the AI jury; store the split + rationale. |
appeal(case_id) |
write | One-round appeal; re-runs the arbiter. |
finalize(case_id) |
write | Release the pot to both parties per the ruling. |
get_case(case_id) |
view | JSON view of a case + both parties. |
get_all_cases() |
view | JSON array of case summaries. |
get_case_count() |
view | Number of cases. |
Shares are stored in basis points (0–10000). The two shares always sum to
10000; finalize gives any rounding remainder to the counterparty so no unit is
lost.
fairsplit/
├── contracts/
│ ├── fair_split.py # the Intelligent Contract (the product)
│ └── storage_test.py # minimal sanity contract to verify Studio first
├── frontend/ # Vite + React + genlayer-js dApp
│ ├── public/ # logo + favicon assets
│ ├── src/
│ │ ├── lib/genlayer.js # client + contract calls
│ │ ├── components/ # BalanceBar, CreateCaseForm, CaseDetail, …
│ │ └── App.jsx
│ └── .env.example
├── tests/
│ ├── conftest.py # shared fixtures + install_mocks helper
│ ├── test_fair_split_fast.py # deterministic paths, no LLM
│ ├── test_fair_split_slow.py # request_ruling / appeal / finalize
│ └── README.md
├── deliverables/ # Explorer submission package
│ ├── SUBMISSION.md
│ ├── SEEDING_PROCEDURE.md
│ ├── logo-1024.png
│ └── logo-512.png
├── scripts/deploy.sh
├── gltest.config.yaml
└── README.md
CLI deployment of Intelligent Contracts isn't the primary path; use the Studio UI.
scripts/deploy.shprints this same checklist.
- Open https://studio.genlayer.com/run-debug
- Settings → Reset Storage → Confirm, then hard refresh the tab.
- (Recommended sanity check) Upload
contracts/storage_test.py, deploy it, click the transaction in the sidebar, and confirm Result: SUCCESS. - Upload
contracts/fair_split.pyand click Deploy (constructor takes no arguments). - Click the deploy transaction in the sidebar and confirm Result: SUCCESS
(not just
Status: FINALIZED). - Copy the deployed contract address — you'll need it for the frontend.
cd frontend
cp .env.example .env
# set VITE_CONTRACT_ADDRESS=<your deployed address> in .env
npm install
npm run dev # opens http://localhost:5173Users connect their own MetaMask wallet and sign every transaction
themselves — no private keys are ever stored in the app. When you click
Connect wallet, the app asks MetaMask to switch to the GenLayer network
(chain id 61999, RPC https://studio.genlayer.com/api, symbol GEN), adding
it automatically if it isn't there yet.
To transact you need some GEN on that network in your wallet. On studionet, fund your address from the GenLayer Studio Accounts panel by transferring from a funded Studio account. Then: connect → open a case → the counterparty joins → ask the AI jury to rule → finalize.
If MetaMask ever shows a
'from'error, it's on the wrong network — reconnect so the app can switch it to GenLayer, and make sure the active account matches the one shown in the app header.
- Push this repo to GitHub.
- Import the repo in Vercel, Root Directory =
frontend(Framework preset: Vite; Build:npm run build; Output:dist). - Add environment variables:
VITE_CONTRACT_ADDRESS— your deployed contract address.VITE_GENLAYER_NETWORK—studionet.
- Deploy. Vercel gives you a live URL.
frontend/vercel.json is included so the build settings are picked up
automatically.
pip install -r tests/requirements.txt
# Fast lane (seconds) — validation, state guards, view methods:
gltest --network localnet -m fast
# or against studionet:
gltest --network studionet -m fast
# Slow lane (~1–2 min) — full request_ruling / appeal / finalize with
# sim_installMocks so the LLM resolves deterministically:
gltest --network localnet -m slow
# Everything:
gltest --network localnetSee tests/README.md for detail.
The ruling runs inside _arbitrate, wrapped by
gl.vm.run_nondet_unsafe(leader_fn, validator_fn):
leader_fnfetches each side's evidence page on-chain (best-effort), builds a prompt containing both arguments, and asks the LLM for{creator_bps, counter_bps, rationale}— normalizing the two shares to sum to 10000.validator_fnre-runsleader_fnon each validator and agrees only if the two verdicts land within 10 percentage points (1000 basis points) of each other. Different wording of the rationale is fine; a materially different split is not. This is the difference between grading meaning and merely grading format.run_nondet_unsafeprovides no sandbox around the validator, so every path invalidator_fnistry / except-guarded and defaults toFalseon any exception — a validator bug can never accidentally pass a bad ruling.
Does the LLM actually run on-chain? Yes — via gl.nondet.exec_prompt inside
the contract. Every validator re-runs their own LLM and votes on whether the
answer is materially the same as the leader's.
What stops one validator from cheating? Consensus. Optimistic Democracy requires the majority of validators to agree on the meaning of the ruling. Free-riding on the leader fails when your own LLM disagrees.
What if the LLM hallucinates a wildly unfair split? The principle demands the two answers be within 10 percentage points — an outlier ruling loses consensus. Either party can also appeal once, which re-runs the arbiter with fresh validators.
Do I need to trust the frontend? For seeing cases, yes; for the ruling, no. Every write is signed by your MetaMask; the frontend cannot forge a ruling.
What if the evidence URL is dead? The arbiter degrades gracefully — a dead URL just removes that page from the LLM's context; both arguments are still read from on-chain storage.
- Deployed on GenLayer studionet (the hosted GenLayer Studio simulator,
chain id
61999, RPChttps://studio.genlayer.com/api). Storage can be reset by GenLayer between runs; if the app ever showsContract not found, the FairSplit contract has to be redeployed andVITE_CONTRACT_ADDRESSrefreshed on Vercel. Fund your MetaMask address from the Accounts panel inside GenLayer Studio — the public Bradbury / Asimov testnet faucet does not work here (different chain). - Deposits are handled in native units; the UI uses plain integers for clarity.
- Evidence fetching is best-effort: a dead URL doesn't crash the ruling, it just removes that page from the evidence the LLM sees.
MIT.