Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FairSplit — an on-chain AI arbiter for splitting shared assets

Network Chain ID Live License

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.

Table of contents

Why this dies without GenLayer

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.

The problem

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.

How FairSplit works

  1. Party A opens a case, escrows a deposit, and states their argument (plus an optional evidence link — receipts, a shared doc, a chat export).
  2. Party B joins, escrows their deposit, and states their side.
  3. 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.
  4. Either party may appeal once, which re-runs the arbiter.
  5. Finalize releases the escrowed pot to both parties in the awarded proportions — trustlessly, straight from the contract.

Architecture

┌────────────────┐   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".

Contract API

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.

Repository layout

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

Deploy the contract (studionet)

CLI deployment of Intelligent Contracts isn't the primary path; use the Studio UI. scripts/deploy.sh prints this same checklist.

  1. Open https://studio.genlayer.com/run-debug
  2. Settings → Reset Storage → Confirm, then hard refresh the tab.
  3. (Recommended sanity check) Upload contracts/storage_test.py, deploy it, click the transaction in the sidebar, and confirm Result: SUCCESS.
  4. Upload contracts/fair_split.py and click Deploy (constructor takes no arguments).
  5. Click the deploy transaction in the sidebar and confirm Result: SUCCESS (not just Status: FINALIZED).
  6. Copy the deployed contract address — you'll need it for the frontend.

Run the frontend locally

cd frontend
cp .env.example .env
# set VITE_CONTRACT_ADDRESS=<your deployed address> in .env
npm install
npm run dev          # opens http://localhost:5173

Signing transactions

Users 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.

Deploy the frontend on Vercel

  1. Push this repo to GitHub.
  2. Import the repo in Vercel, Root Directory = frontend (Framework preset: Vite; Build: npm run build; Output: dist).
  3. Add environment variables:
    • VITE_CONTRACT_ADDRESS — your deployed contract address.
    • VITE_GENLAYER_NETWORKstudionet.
  4. Deploy. Vercel gives you a live URL.

frontend/vercel.json is included so the build settings are picked up automatically.

Run the tests

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 localnet

See tests/README.md for detail.

How consensus actually works here

The ruling runs inside _arbitrate, wrapped by gl.vm.run_nondet_unsafe(leader_fn, validator_fn):

  • leader_fn fetches 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_fn re-runs leader_fn on 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_unsafe provides no sandbox around the validator, so every path in validator_fn is try / except-guarded and defaults to False on any exception — a validator bug can never accidentally pass a bad ruling.

FAQ

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.

Notes & limits

  • Deployed on GenLayer studionet (the hosted GenLayer Studio simulator, chain id 61999, RPC https://studio.genlayer.com/api). Storage can be reset by GenLayer between runs; if the app ever shows Contract not found, the FairSplit contract has to be redeployed and VITE_CONTRACT_ADDRESS refreshed 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.

License

MIT.

About

FairSplit — an on-chain AI arbiter that escrows a shared pot on GenLayer and lets a decentralized LLM jury rule on a fair split when people part ways.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages