A full Python/FastAPI port of the n8n AI Arbitration Platform.
Automated contract arbitration backed by PostgreSQL, LangChain AI agents, and Ethereum escrow.
- What This Is
- How It Works — The Full Lifecycle
- Architecture
- Prerequisites
- Installation & Setup
- Running the Server
- Using the Platform — Step by Step
- API Reference
- Environment Variables
- Status Model
- Financial Model
- Project Structure
- Production Deployment Notes
The AI Arbitration Platform is a smart-contract-style legal arbitration system. Two parties — a Seller (provider) and a Buyer (payer) — enter a binding contract. The Buyer deposits ETH into escrow. If they cannot resolve a dispute themselves, a two-stage AI system (Magistrate Judge + Final Judge) investigates the case and issues a legally binding award. Funds are distributed automatically on-chain after a 7-day objection window.
This repository is a Python/FastAPI implementation that mirrors the logic of the original n8n workflows exactly, replacing Google Sheets with PostgreSQL and Google Drive with local file storage.
┌─────────────────────────────────────────────────────────────────────┐
│ ARBITRATION LIFECYCLE │
├──────────┬──────────────────────────────────────────────────────────┤
│ Phase 1 │ Contract submitted → Case ID generated → Emails sent │
│ Phase 2 │ Both parties accept contract + register ETH wallets │
│ Phase 3 │ Buyer deposits escrow to on-chain wallet → Verified │
│ Phase 4 │ Seller requests payment OR Buyer requests refund │
│ Phase 5 │ Opposing party disputes → Evidence window opens (7 days) │
│ Phase 6 │ AI Magistrate investigates → AI Final Judge rules │
│ Phase 7 │ 7-day objection window → Award distributed on-chain │
└──────────┴──────────────────────────────────────────────────────────┘
Each phase maps to one of the five workflow modules:
| Module | Phase | n8n Equivalent |
|---|---|---|
initialization.py |
1–2 | Initialization.json |
transactions.py |
3–4 | Transactions.json |
prosecution.py |
4–5 | Prosecution.json |
adjudication.py |
6 | Adjudication.json |
objection.py |
7 | Objection.json |
exceptions.py |
Global | Exceptions.json |
┌─────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Routes │ │ Templates│ │ Static │ │
│ │ (6 mods) │ │ (Jinja2) │ │ (CSS) │ │
│ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ │
│ ┌────▼──────────────────────────────────────────┐ │
│ │ SQLAlchemy ORM (models.py) │ │
│ └────┬──────────────────────────────────────────┘ │
│ │ │
└───────┼─────────────────────────────────────────────────┘
│
┌───────▼──────────┐ ┌──────────────────────────────┐
│ PostgreSQL DB │ │ External Services (mock) │
│ (Docker :5433) │ │ Etherscan · Tatum · OpenAI │
└──────────────────┘ └──────────────────────────────┘
Key design choices:
- PostgreSQL via Docker replaces Google Sheets as the case ledger
- Local
uploads/replaces Google Drive for file uploads - LangChain + OpenAI / Gemini / DeepSeek powers the two-stage AI adjudication. The Magistrate Judge uses a heavy, multimodal model (e.g., GPT-4o or Gemini 1.5 Pro) to parse evidence, while the Final Judge uses a fast, text-only model (e.g., GPT-4o-mini or Gemini 1.5 Flash) for quick, reliable JSON extraction and ruling logic. Both stages use
with_fallbacks()for multi-provider resilience. - Agentic Evidence Tooling: The AI Magistrate runs as a LangChain ReAct agent equipped with a
read_evidence_filetool to directly parse PDF/TXT evidence. - Gmail IMAP Ingestion: A built-in IMAP client automatically polls for email replies, extracts Case IDs, hashes attachments, and saves them to the case ledger.
- APScheduler: Manages automated cron jobs for timeouts, evidence window expirations, and Gmail polling.
- Blockchain verified: Actual Etherscan API validation replaces mock deposits.
- Email is sent via SMTP: Powered by
email_service.pyusing standard SMTP credentials.
Before you start, make sure you have:
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.9+ | Runtime |
| Docker Desktop | Latest | PostgreSQL container |
| Git | Any | Cloning the repo |
| OpenAI API Key | — | AI adjudication (Phase 6 only) |
Docker Desktop must be running before you start the database.
git clone https://github.com/just-bots/ai-arbitration-python.git
cd ai-arbitration-pythoncp .env.example .envOpen .env and fill in your values. At minimum, set OPENAI_API_KEY for AI adjudication to work. Everything else has safe defaults for local development.
# Minimum required for local dev:
OPENAI_API_KEY=sk-...your-key-here...
# Optional — defaults are fine for local testing:
BASE_URL=http://localhost:8000/
ADMIN_EMAIL=admin@example.com
ESCROW_WALLET=0x0000000000000000000000000000000000000000
PROCESSING_FEE=1000000000000000 # 0.001 ETH in Wei
DATABASE_URL=postgresql://postgres:password@localhost:5433/arbitrationSee Environment Variables for the full list.
docker-compose up -dThis starts a PostgreSQL 15 container named arbitration-db on port 5433.
To verify it's running:
docker ps
# You should see: arbitration-db postgres:15 Up ... 0.0.0.0:5433->5432/tcppip install -r requirements.txtIf you're on macOS and get a
psycopg2error, try:pip install psycopg2-binary
python create_tables.pyExpected output:
Database schema created successfully.
This creates three tables: cases, messages, and files.
Connect with any SQL client using:
| Setting | Value |
|---|---|
| Host | 127.0.0.1 |
| Port | 5433 |
| Database | arbitration |
| Username | postgres |
| Password | password |
Recommended clients: TablePlus, DBeaver, pgAdmin
python -m uvicorn main:app --reload --port 8000The server is now live at http://localhost:8000
| URL | Description |
|---|---|
| http://localhost:8000/ | Case creation form (start here) |
| http://localhost:8000/docs | Auto-generated Swagger API docs |
| http://localhost:8000/redoc | ReDoc API docs |
The --reload flag makes the server automatically restart when you edit any Python file.
Important: In local development, emails are not actually sent.
All email links are printed to the terminal/console where the server is running.
Watch the terminal output — every link you need for the next step will appear there.
Who does this: The platform operator or one of the parties
- Open http://localhost:8000/
- Fill in the registration form:
- Seller name + email — the service provider (e.g., contractor, freelancer, landlord)
- Buyer name + email — the payer (e.g., client, employer, renter)
- Seller ETH wallet — Ethereum address for receiving awards
- Buyer ETH wallet — Ethereum address for receiving refunds
- Required Escrow Fund (ETH) — the amount the Buyer must deposit (e.g.,
0.5) - Contract file — upload the PDF of the contract
- Click Submit
What happens:
- A unique
Case IDis generated (e.g.,CASE-3F7A2B1C) - Party tokens are created for secure link authentication
- The case is saved to the database with
status = PENDING - Escrow fund is stored in Wei (
ETH × 10¹⁸) - Two simulated emails are printed to the terminal:
--- EMAIL SIMULATION --- To: seller@example.com Subject: Action Required: Contract Registered (CASE-3F7A2B1C) Link: http://localhost:8000/response?caseId=CASE-3F7A2B1C&party=Seller&action=accept&token=... Wallet: http://localhost:8000/wallet?caseId=CASE-3F7A2B1C&party=Seller&token=... To: buyer@example.com Subject: Action Required: Contract Registered (CASE-3F7A2B1C) Link: http://localhost:8000/response?caseId=CASE-3F7A2B1C&party=Buyer&action=accept&token=...
Who does this: Both Seller and Buyer independently
Each party uses their unique link from the terminal:
http://localhost:8000/response?caseId=CASE-...&party=Seller&action=accept&token=...
http://localhost:8000/response?caseId=CASE-...&party=Buyer&action=accept&token=...
To decline, change action=accept to action=decline in the URL.
What happens:
- The party's response is recorded in the database
- When both parties accept →
status = SIGNED - If either party declines →
status = DECLINED
Parties can submit or update their Ethereum wallet address:
http://localhost:8000/wallet?caseId=CASE-...&party=Seller&token=...
http://localhost:8000/wallet?caseId=CASE-...&party=Buyer&token=...
The address is validated against the Ethereum format (0x + 40 hex characters).
Who does this: The Buyer
In production, the Buyer sends ETH to the ESCROW_WALLET address with the Case ID encoded in the transaction data field. In local development, this is mocked.
To simulate a deposit locally:
http://localhost:8000/transactions/verify?caseId=CASE-...
What happens:
- The system checks if the deposited amount meets
Escrow Fund + Processing Fee - If sufficient:
deposited_fundis recorded,statusstaysSIGNED - If the deposit exceeds the required amount, Excess Funds are detected. The buyer receives dedicated links to either Withdraw Excess or Tip the Seller.
- The terminal prints the next-step links for both parties:
--- EMAIL SIMULATION: FUNDING CONFIRMED --- To: seller@example.com Action: Release Payment -> http://localhost:8000/transactions/action?caseId=...&token=...&actionType=release_payment To: buyer@example.com Action: Request Refund -> http://localhost:8000/transactions/action?caseId=...&token=...&actionType=request_refund
Who does this: Either party, depending on the outcome
Once funded, the two parties have a chance to settle without a dispute:
The Buyer uses their token link with actionType=release_payment:
http://localhost:8000/transactions/action?caseId=CASE-...&token={buyer_token}&actionType=release_payment
What happens:
- The remaining escrow balance is calculated
payment_to_selleris incremented by the remittance amount- If the full escrow is paid:
status = CLOSED - A simulated blockchain transfer is logged to the terminal
http://localhost:8000/transactions/action?caseId=CASE-...&token={buyer_token}&actionType=request_refund
What happens:
status = DISPUTEDdispute_timeandrefund_request_timeare both recorded- A
Disputemessage is logged in the messages table - This triggers the 7-day evidence window (Phase 5)
Who does this: Both parties submit arguments and supporting files
Both Seller and Buyer can submit arguments and evidence files:
http://localhost:8000/prosecution/evidence?caseId=CASE-...&token={party_token}
On this form:
- Write your argument in the text area
- Attach supporting files (PDFs, images, documents)
- Click Submit Evidence
What happens:
- The argument is saved as a
Messagerecord withlabel = Dispute - Each file is SHA-256 hashed, renamed securely, and saved to
storage/evidence/ - File metadata is recorded in the
filestable linked to the message
When the 7-day evidence window closes (or you want to manually escalate for testing):
# POST request to escalate
curl -X POST http://localhost:8000/prosecution/escalate \
-d "caseId=CASE-..."Or visit /docs and use the Swagger UI.
What happens:
status = DISPUTED(confirming ready for AI pickup)dispute_timeis recorded (if not already set)- The AI adjudication scheduler will pick this up in Phase 6
Who does this: The AI (triggered by the operator / admin)
⚠️ RequiresOPENAI_API_KEYto be set in.env
The AI adjudication is a two-stage process:
Investigates the case impartially — reads all messages, evidence files, and the contract. Produces a structured report with:
- Verified facts (each citing its source)
- Contradictions between parties
- Unsubstantiated claims
- Recommended payout split (in Wei)
Reviews the Magistrate's report and issues a legally binding ruling with:
- Final decision text
buyer_awardandseller_awardin Wei- Detailed rationale
- Confidence score (0.0 – 1.0)
Math validation: The system enforces buyer_award + seller_award == escrow_balance exactly. If the AI fails this check, the admin is alerted and the ruling is rejected.
curl -X POST http://localhost:8000/adjudication/run \
-d "caseId=CASE-..."Or use the Swagger UI at http://localhost:8000/docs → POST /adjudication/run
What happens:
- Case is locked immediately:
status = PROCESSING,adjudication_timerecorded - All messages and files are loaded from the database
- A structured case packet is built and sent to the Magistrate LLM
- Magistrate report is validated (math + format)
- Final Judge receives the Magistrate report and issues a ruling
- Ruling is validated:
buyer_award + seller_awardmust equalescrow_balance - On success:
status = DECIDEDdetermination_time,decision,seller_award,buyer_awardstored- Simulated determination emails printed to terminal with 7-day appeal link
What the terminal prints:
--- EMAIL SIMULATION: FINAL DETERMINATION ---
To: seller@example.com | To: buyer@example.com
Subject: Final Determination for Case CASE-...
Decision: [AI ruling text]
Seller Award: X ETH | Buyer Award: Y ETH
Appeal Link (valid 7 days): http://localhost:8000/objection/appeal?caseId=...&token=...
Who does this: Either party (objection), then the Admin (review), then the system (distribution)
Either party has 7 days from determination_time to file a procedural objection:
http://localhost:8000/objection/appeal?caseId=CASE-...&token={party_token}
On the form:
- Describe the procedural or logical error in the ruling
- Check "I confirm this objection contains no new argument"
- Click Submit
⚠️ Objections are limited to procedural/logical errors. No new evidence or arguments are permitted.
What happens:
- Objection text is saved as an
APPEALmessage appeal_timeis recorded- A simulated admin alert is printed to the terminal with the review portal link
The admin reviews the objection at:
http://localhost:8000/objection/review?caseId=CASE-...
The admin sees the full case, the AI ruling, and the objection text.
The admin selects one of two decisions:
| Decision | Action | n8n equivalent |
|---|---|---|
| Uphold | Original ruling stands; case returns to DECIDED for distribution |
Uphold Determination |
| Reverse | Ruling wiped; case returns to DISPUTED for re-adjudication |
Revert Determination |
Submit via:
# Uphold
curl -X POST http://localhost:8000/objection/review \
-d "caseId=CASE-...&action=uphold"
# Reverse
curl -X POST http://localhost:8000/objection/review \
-d "caseId=CASE-...&action=reverse"In n8n, the Objection.json workflow runs on a hourly schedule and automatically distributes ETH after 7 days. In this Python implementation, distribution is triggered via the review endpoint (uphold path) or can be called directly.
When distribution runs:
seller_awardETH is transferred toseller_walletbuyer_awardETH is transferred tobuyer_walletstatus = CLOSED- Both parties receive a confirmation email with the Etherscan transaction link
In local dev, blockchain transfers are mocked (logged to terminal). In production, set
PRIVATE_KEYandTATUM_API_KEYfor real on-chain transfers.
All endpoints are also browsable at http://localhost:8000/docs
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/ |
None | Case creation form |
POST |
/create-case |
None | Submit new contract |
GET |
/success/{case_id} |
None | Case created confirmation |
GET |
/response |
token |
Accept or decline contract |
GET |
/wallet |
token |
Wallet submission form |
POST |
/wallet-submit |
token |
Save Ethereum wallet address |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/transactions/verify |
caseId |
Verify escrow deposit (mock) |
GET |
/transactions/action |
token |
Release payment or request refund |
/transactions/action query parameters:
| Parameter | Values | Who |
|---|---|---|
caseId |
Case ID string | — |
token |
Party's unique token | — |
actionType |
release_payment |
Buyer only |
actionType |
request_refund |
Buyer only |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/prosecution/evidence |
token |
Evidence submission form |
POST |
/prosecution/evidence |
token |
Submit argument + files |
POST |
/prosecution/escalate |
None | Escalate case to DISPUTED |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/adjudication/run |
None | Run two-stage AI adjudication |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/objection/appeal |
token |
Procedural objection form |
POST |
/objection/appeal |
token |
Submit objection |
GET |
/objection/review |
Admin | HITL review dashboard |
POST |
/objection/review |
Admin | Uphold or reverse ruling |
Copy .env.example to .env and configure these variables:
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | postgresql://postgres:password@localhost:5433/arbitration |
PostgreSQL connection string |
BASE_URL |
Yes | http://localhost:8000/ |
Public base URL for tokenized email links |
ADMIN_EMAIL |
Yes | admin@example.com |
Recipient of all system error alerts |
ESCROW_WALLET |
Yes | 0x000...000 |
Ethereum wallet receiving escrow deposits |
PRIVATE_KEY |
Prod only | — | Private key for signing payout transactions (never commit) |
PROCESSING_FEE |
Yes | 1000000000000000 |
Platform fee in Wei (default = 0.001 ETH) |
OPENAI_API_KEY |
Phase 6 | — | OpenAI API key for LangChain AI agents |
ETHERSCAN_API_KEY |
Prod only | — | For real deposit verification |
TATUM_API_KEY |
Prod only | — | For broadcasting ETH payout transactions |
SMTP_HOST |
Prod only | — | SMTP server for email fallback |
SMTP_PORT |
Prod only | 587 |
SMTP port |
SMTP_USER |
Prod only | — | SMTP login email |
SMTP_PASS |
Prod only | — | SMTP password |
⚠️ PRIVATE_KEYcontrols the escrow wallet. Store it in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) — never in.envfiles committed to version control.
| Status | Meaning | Next transition |
|---|---|---|
PENDING |
Case created; awaiting both signatures | → SIGNED or DECLINED |
SIGNED |
Both accepted; awaiting escrow deposit | → DISPUTED |
DECLINED |
At least one party declined | Terminal |
DISPUTED |
Dispute filed; evidence window open | → PROCESSING |
PROCESSING |
AI adjudication in progress (case locked) | → DECIDED |
DECIDED |
AI ruling issued; 7-day appeal window | → CLOSED or → DISPUTED |
CLOSED |
Funds distributed; case complete | Terminal |
All monetary values are stored as Wei integers in Numeric(38, 0) columns.
1 ETH = 1,000,000,000,000,000,000 Wei (10¹⁸)
Escrow balance formula:
escrow_balance = max(0, Escrow Fund − Payment to Seller − Refund to Buyer)
Liquid balance formula (available funds in the deposit pot):
liquid_balance = Deposited Fund − Fee − Tip to Seller − Buyer Withdrawal
− Payment to Seller − Refund to Buyer
AI award validation (strictly enforced):
buyer_award + seller_award == escrow_balance ← must be exact
buyer_award >= 0
seller_award >= 0
ai-arbitration-python/
│
├── main.py # FastAPI app entry point
├── models.py # SQLAlchemy ORM models (Case, Message, File)
├── database.py # DB session factory
├── create_tables.py # One-time schema initializer
├── docker-compose.yml # PostgreSQL container
├── requirements.txt # Python dependencies
├── .env.example # Environment variable template
│
├── routers/
│ ├── initialization.py # Phase 1–2: Case creation, signatures, wallets
│ ├── transactions.py # Phase 3–4: Deposit verification, payments
│ ├── prosecution.py # Phase 4–5: Evidence upload, dispute escalation
│ ├── adjudication.py # Phase 6: AI Magistrate + Final Judge
│ ├── objection.py # Phase 7: Appeals, HITL review, distribution
│ └── exceptions.py # Global: Error handler, admin alerts
│
├── templates/ # Jinja2 HTML templates
│ ├── layout.html
│ ├── create_case.html
│ ├── success.html
│ ├── response_status.html
│ ├── wallet_form.html
│ ├── deposit_status.html
│ ├── transaction_action.html
│ ├── evidence_form.html
│ ├── evidence_success.html
│ ├── adjudication_result.html
│ ├── objection_form.html
│ └── objection_review.html
│
├── static/
│ └── css/style.css # Platform stylesheet
│
├── storage/
│ └── evidence/ # Uploaded evidence files (SHA-256 hashed)
│
└── uploads/ # Contract PDF uploads
Replace the print() statements in each router with an SMTP client or transactional email provider (SendGrid, Mailgun, Postmark).
Note: The global exception handler (
exceptions.py) is already fully wired to send admin SMTP alerts for unhandled errors and AI adjudication math/schema validation failures, ensuring a zero-trust alert system.
- Set
ETHERSCAN_API_KEYto enable real deposit scanning via the Etherscan API - Set
TATUM_API_KEY+PRIVATE_KEYto enable real ETH payout broadcasting via Tatum v3
Replace storage/evidence/ with Google Drive, AWS S3, or equivalent. Update the file upload/download logic in prosecution.py and adjudication.py.
The n8n workflows run adjudication on a 12-hour schedule and distribution on an hourly schedule. In production, set up cron jobs or a task queue (Celery, APScheduler) to call:
POST /adjudication/runevery 12 hours (picks up DISPUTED cases ≥ 7 days old)- Award distribution logic every hour (picks up DECIDED cases ≥ 7 days old)
- Serve over HTTPS (use a reverse proxy: Nginx, Caddy, or Traefik)
- Move
PRIVATE_KEYto a secrets manager - Rate-limit token-authenticated endpoints
- Add authentication to
/adjudication/runand/objection/review
The adjudication router is already configured with separated models for efficiency:
- Magistrate Judge →
gpt-4o(multimodal, capable of reading evidence) with fallbacks togemini-1.5-proandgpt-4o-mini. - Final Judge →
gpt-4o-mini(text-only, fast, efficient JSON output) with fallbacks todeepseek-chat,gemini-1.5-flash, andgpt-3.5-turbo.
Update the model initialization in adjudication.py to use your preferred provider.