◆ NETWORK STATUS
VALIDATORS5/5
PENDING TXS4
LAST BLOCK#4,821,094
AVG VERDICT11.2s
HERO
Documentation

DOCUMENTATION

How Arbiq works under the hood — contracts, hooks, AI evaluation, and the full job lifecycle.

Introduction

HOW ARBIQ WORKS

Payment locked in escrow at post time, released by AI consensus at delivery time — no intermediary needed.

Arbiq is built on GenLayer — a blockchain that runs Intelligent Contracts: Python smart contracts that can call LLMs and reach non-deterministic consensus across independent validators. Arbiq uses this to have AI evaluate freelance work and automatically settle payment without a trusted third party.

When a client posts a job, their GEN tokens lock into the contract immediately. When the freelancer submits evidence of completed work, the client triggers an AI evaluation — multiple GenLayer validators independently read the job spec and evidence URL, compare results, and if they agree, execute payment atomically. No cut, no human arbiter, no payment delays.

Trustless Escrow

Funds locked at post time. Released only on AI approval or manual client sign-off.

AI Enforcement

GenLayer validator consensus runs LLMs to evaluate evidence. No single point of control.

Zero Platform Fee

100% of escrowed GEN goes to the freelancer on approval. No middleman percentage.

Testnet only. Arbiq runs on GenLayer Bradbury Testnet (Chain ID 4221). GEN tokens have no real monetary value. The network is experimental.
Getting Started

QUICK START

From zero to posting your first job in under 5 minutes.

01

Install a wallet

Install MetaMask or any WalletConnect-compatible wallet. Arbiq uses RainbowKit and supports injected wallets, WalletConnect, and Coinbase Wallet.

02

Add GenLayer Bradbury Testnet

Arbiq will prompt you to switch networks automatically when you connect. Accept the network switch.

text
Network name: GenLayer Bradbury Testnet
Chain ID:     4221
RPC URL:      https://rpc-bradbury.genlayer.com
Symbol:       GEN
Explorer:     https://explorer-bradbury.genlayer.com
03

Get testnet GEN tokens

Visit the GenLayer Studio faucet to receive testnet GEN. You need GEN to post jobs (budget is held in escrow).

04

Post a job

Go to /jobs/new, fill in the title, description, deadline, and budget. Click "Post Job" — your wallet will prompt you to sign a transaction. The budget is locked in the contract immediately.

05

Wait for a freelancer

Your job appears on /jobs. Any wallet can browse and accept it. You'll receive a notification when someone takes the job.

System Design

ARCHITECTURE

How the browser, genlayer-js, and the GenLayer validator network fit together.

Browser (Next.js 16 · React 19 · Tailwind CSS v4)
   │
   ├─ wagmi v2 + RainbowKit  ──── wallet signing (MetaMask, WalletConnect…)
   ├─ TanStack Query v5       ──── caches chain reads, auto-refetches
   └─ genlayer-js v1.1.8      ──── encodes calls, polls consensus status
        │
        │  readContract()  → gen_call(type:"read")   ─── view methods
        │  writeContract() → eth_sendTransaction      ─── state mutations
        │  getTransaction()→ poll every 1s            ─── consensus tracking
        │
        ▼
GenLayer Bradbury Testnet  (Chain ID 4221)
   │
   ├─ Consensus Main Contract  ←── all transactions land here first
   │     PROPOSING → COMMITTING → REVEALING → ACCEPTED
   │
   └─ arbiq.py  (Intelligent Contract · Python)
         ├─ job_count : u256
         ├─ jobs      : TreeMap[u256, str]    ← JSON job objects
         └─ messages  : TreeMap[u256, str]    ← JSON message arrays

Read calls go directly to an RPC node via gen_call(type:"read") — no wallet needed, instant response. Write calls are msgpack-encoded and sent as eth_sendTransaction to the consensus contract. The frontend then polls getTransaction() every second until the transaction reaches a decided state (ACCEPTED or FINALIZED_ERROR).

Why poll at 1 second? The built-in waitForTransactionReceipt sleeps 3s between polls with no intermediate status. Custom polling surfaces live consensus phases (PROPOSING, COMMITTING, REVEALING) and reaches finality ~3× faster.
Smart Contract

CONTRACT REFERENCE

All methods on arbiq.py — the GenLayer Intelligent Contract.

v2 · Active since Jun 7, 2026GenLayer Bradbury Testnet · Chain ID 4221
v2

Current — proposals, cancel & refunds, deadlines, disputes, profiles, ratings, security hardening

v1

Superseded — escrow, AI evaluation, milestones, chat

GenLayer contracts are immutable — an upgrade means deploying a new contract and repointing the app. Jobs on a previous deployment stay readable at its address.

The contract is a Python class inheriting from gl.Contract. State is stored in TreeMapfields — GenLayer's on-chain key-value store — with values serialized as JSON strings.

python
class Arbiq(gl.Contract):
    job_count : u256                   # auto-incrementing ID counter
    jobs      : TreeMap[u256, str]     # job_id → JSON job object
    messages  : TreeMap[u256, str]     # job_id → JSON message array
    proposals : TreeMap[u256, str]     # job_id → JSON proposal array
    profiles  : TreeMap[str,  str]     # address → JSON profile object

Write Methods

post_job(title, description, deadline)payable write

title: str · description: str · deadline: str · value: GEN (required)

Creates a new job. The GEN sent with the transaction is locked in escrow. Title must be ≥ 3 chars, description ≥ 20 chars. Job starts in OPEN status.

post_job_milestones(title, description, deadline, milestone_titles)payable write

… · milestone_titles: list (2–5) · value: GEN (required)

Creates a milestone job. The escrow is split evenly across 2–5 milestones, each delivered and approved (and paid) independently.

apply_to_job(job_id, note, bid)write

job_id: int · note: str (required) · bid: int (0 = at budget)

Freelancer submits a proposal for an OPEN job. One proposal per address. The bid is informational — escrow stays at the posted budget.

accept_proposal(job_id, freelancer)write

job_id: int · freelancer: str

Client assigns one of the applicants. The address must have applied. Transitions status to ACTIVE. This is the primary assignment flow.

take_job(job_id)write

job_id: int

Direct self-assignment (no-proposal fallback). Job must be OPEN; the caller cannot be the client. Transitions status to ACTIVE.

cancel_job(job_id)write

job_id: int

Client cancels an OPEN (unassigned) job and the escrow is refunded. Status becomes CANCELLED.

submit_delivery(job_id, evidence_url, evidence_note)write

job_id: int · evidence_url: str (required) · evidence_note: str

Freelancer marks the job as delivered with a URL pointing to the work (GitHub, live site, Figma, Loom, etc.). Transitions status to DELIVERED.

auto_evaluate(job_id)non-deterministic write

job_id: int · client-only

Client triggers AI consensus evaluation. Validators independently fetch the evidence URL and run an LLM rubric. Untrusted submission content is fenced against prompt injection. If approved, funds transfer automatically. Status becomes COMPLETED or DISPUTED.

release_manually(job_id)write

job_id: int

Client bypasses AI and approves payment directly. Job must be DELIVERED. Only the client can call this. Funds transfer immediately. Status becomes COMPLETED.

resubmit_delivery(job_id, evidence_url, evidence_note)write

job_id: int · evidence_url: str (required) · evidence_note: str

Freelancer resubmits stronger evidence on a DISPUTED job (max 2 resubmits). Status returns to DELIVERED for re-evaluation.

reclaim_expired(job_id)write

job_id: int

Client reclaims escrow when an assigned freelancer never delivered and the deadline has passed (ghost protection). Status becomes CANCELLED.

reclaim_disputed(job_id)write

job_id: int

Client reclaims escrow on a DISPUTED job once the freelancer has exhausted all resubmits. Status becomes REFUNDED — funds are never permanently locked.

rate_freelancer(job_id, stars, review)write

job_id: int · stars: int (1–5) · review: str

Client rates the freelancer on a COMPLETED job (one rating per job). The average is aggregated into the freelancer's on-chain profile.

set_profile(display_name, bio, skills)write

display_name: str · bio: str · skills: list

Any address sets its own public profile. Reputation, earnings, and ratings are computed by the contract and can never be forged here.

send_message(job_id, content)write

job_id: int · content: str (max 500 chars)

Appends a message to the job's chat thread. Only the client or assigned freelancer can message. Job must not be OPEN (messaging starts after a freelancer is assigned).

View Methods

get_job(job_id)view

job_id: int → str (JSON)

Returns a single job as a JSON string. Returns empty string if the job does not exist.

get_all_jobs()view

→ str (JSON array)

Returns all jobs ever posted as a JSON array. Used by the browse page for client-side filtering and sorting.

get_jobs_page(offset, limit)view

offset: int · limit: int (clamped ≤ 50) → str (JSON)

Paginated, newest-first job read. Returns { total, offset, limit, jobs }. Keeps reads bounded as the job count grows.

get_proposals(job_id)view

job_id: int → str (JSON array)

Returns the proposals submitted for a job. Returns "[]" if none.

get_profile(address)view

address: str → str (JSON)

Returns the address's profile: display name, bio, skills, reputation score, completed/disputed counts, total earned, and average star rating.

get_jobs_by_client(address)view

client: str → str (JSON array)

Returns all jobs posted by the given wallet address. Case-insensitive match.

get_jobs_by_freelancer(address)view

freelancer: str → str (JSON array)

Returns all jobs taken by the given wallet address. Used by the notification system.

get_messages(job_id)view

job_id: int → str (JSON array)

Returns the chat message array for a job as a JSON string. Returns "[]" if no messages exist.

get_job_count()view

→ int

Returns the total number of jobs ever posted (includes all statuses). Used by the landing page stats.

Job States

JOB LIFECYCLE

How a job moves through states from creation to completion.

  post_job()
      │
      ▼
  ┌────────┐   cancel_job()                    ┌───────────┐
  │  OPEN  │ ─────────────────────────────────►│ CANCELLED │ (escrow refunded)
  └────────┘                                   └───────────┘
      │
      │  apply_to_job()  →  accept_proposal()      (or take_job)
      ▼
  ┌────────┐   reclaim_expired()  (deadline missed)
  │ ACTIVE │ ──────────────────────────────────► CANCELLED  (escrow refunded)
  └────────┘
      │  submit_delivery()
      ▼
  ┌───────────┐
  │ DELIVERED │
  └───────────┘
      │
      ├─ auto_evaluate()  ─ approved ─►  COMPLETED   (freelancer paid)
      │                  └ rejected ─►  DISPUTED
      │
      └─ release_manually()         ─►  COMPLETED   (freelancer paid)

  COMPLETED  ── rate_freelancer() ──►  ★ on-chain rating

  DISPUTED   ── resubmit_delivery() (≤ 2×) ──►  back to DELIVERED
             └─ reclaim_disputed()  (resubmits used) ──►  REFUNDED
OPENapply_to_job() → accept_proposal()by Freelancer / Clientactive
OPENcancel_job()by Clientcancelled (refund)
ACTIVEsubmit_delivery()by Assigned freelancerdelivered
ACTIVEreclaim_expired()by Client (past deadline)cancelled (refund)
DELIVEREDauto_evaluate()by Clientcompleted or disputed
DELIVEREDrelease_manually()by Clientcompleted
DISPUTEDresubmit_delivery()by Freelancer (≤ 2×)delivered
DISPUTEDreclaim_disputed()by Client (resubmits used)refunded
COMPLETEDrate_freelancer()by Clientcompleted (★ rated)
DISPUTED status means the AI evaluated the delivery and rejected it. The freelancer can resubmit_delivery() with stronger evidence up to twice. If all resubmits are exhausted without approval, the client can reclaim_disputed() to refund the escrow — funds are never permanently locked.
AI System

AI EVALUATION

How GenLayer validators reach consensus on freelance work quality.

auto_evaluate is the only non-deterministiccontract method. It uses GenLayer's gl.eq_principle.strict_eq to ensure all validator nodes must produce the exact same result before the transaction finalizes.

python
def evaluate() -> str:
    prompt = f"""You are an impartial AI judge evaluating freelance work.

JOB: {title}
DESCRIPTION: {description}
BUDGET: {budget} GEN

SUBMISSION:
- Evidence URL:  {evidence_url}
- Freelancer note: {evidence_note}

Respond ONLY with this JSON:
{{"approved": bool, "reasoning": str}}"""

    result = gl.nondet.exec_prompt(prompt)
    return result

result_str = gl.eq_principle.strict_eq(evaluate)
# All validators must agree on this string before tx finalizes

What AI checks

  • Is the evidence URL relevant to the job spec?
  • Does the freelancer note describe completing the work?
  • Is there a reasonable basis to believe the work is done?

What AI does NOT check

  • Code quality or test coverage
  • Whether the URL is currently live
  • Long-term maintenance or edge cases

Consensus phases during AI evaluation

PROPOSINGLeader validator runs the LLM prompt against your evidence
COMMITTINGEach other validator independently runs the same prompt and hashes the result
REVEALINGValidators reveal their hashes — all must match (strict_eq)
ACCEPTEDSupermajority agreed — payment transfer executes atomically
AI evaluation typically takes 1–5 minutes because each validator independently calls an LLM and they must all agree. The UI polls every second and shows live phase progress. Other contract writes (take job, submit delivery) finalize in 15–45 seconds.
Quality Assurance

TESTING

105 unit tests using the real GenVM SDK — no testnet needed.

The contract ships with a full test suite using gltest direct mode — the official GenLayer test runner that loads the real genlayer-py-std SDK locally from a GitHub release cache. Tests run in ~2 seconds with no wallet and no RPC.

bash
# Run everything: lint + full test suite
npm run check

# Fast AST lint only (~0.1s)
npm run lint

# Full test suite only (~2s)
npm test

Test Coverage — 105 tests across 13 classes

TestPostJob11Validation, field defaults, whitespace stripping
TestPostJobMilestones9Budget split, remainder, min/max milestone guards
TestTakeJob6Happy path, self-take, double-take, nonexistent job
TestSubmitDelivery7Auth, open-job guard, blank/whitespace URL
TestSubmitMilestoneDelivery9Index bounds, non-milestone job, already-delivered guard
TestApproveMilestone9Payment per milestone, completion, double-approve guard
TestAutoEvaluate15Score avg logic, avg=6 boundary, AI JSON parsing, profiles
TestReleaseManually6Auth, reasoning field, profile update
TestResubmitDelivery6Field clearing, max-2 resubmit cap
TestMessaging8Accumulation, 500-char truncation, open-job guard
TestGetProfile5Defaults, reputation score formula (completed / total)
TestViewMethods9All view helpers, case-insensitive address filter
TestFullLifecycle6End-to-end: AI, manual, dispute→resubmit, milestones

Mocking AI & web calls

python
# Mock any LLM prompt — regex pattern, JSON string response
direct_vm.mock_llm(".*", json.dumps({
    "approved": True,
    "reasoning": "Work looks good.",
    "scores": {
        "relevance": 8, "completeness": 8,
        "quality": 8, "meets_spec": 8, "professional": 8,
    },
    "confidence": "high",
}))

# Mock evidence URL fetch — regex pattern, response dict
direct_vm.mock_web(".*github.*", {
    "method": "GET", "status": 200, "body": "# Project\nAll done."
})

# Clear between phases when a test needs different responses
direct_vm.clear_mocks()

Score-based approval logic

The contract ignores the "approved" field from the LLM and recomputes it from the 5 numeric scores. Tests cover all three boundary cases:

avg=4.6DISPUTEDrelevance:4 completeness:5 quality:5 meets_spec:4 professional:5
avg=6.0COMPLETEDall scores exactly 6 — boundary pass
avg=8.0COMPLETEDall scores 8 — normal approval
python
avg = sum(scores.values()) / 5   # 5 criteria, each 0–10
approved = avg >= 6.0             # strict ≥ 6 threshold
genvm-lint runs 3 AST safety checks on the contract before tests execute. If lint fails, the test step is skipped. Run npm run lint standalone for a fast pre-commit check.
Wallet Setup

WALLET & NETWORK

Connecting, signing, and staying on the right chain.

Arbiq uses RainbowKit v2 with wagmi v2. Any EIP-1193 compatible wallet works — MetaMask, Coinbase Wallet, WalletConnect-compatible mobile wallets. The app is locked to Bradbury Testnet only.

text
Network name:  GenLayer Bradbury Testnet
Chain ID:      4221
RPC URL:       https://rpc-bradbury.genlayer.com
Currency:      GEN (18 decimals)
Explorer:      https://explorer-bradbury.genlayer.com

Automatic network switch

When you connect your wallet, Arbiq checks the active chain. If it's not Bradbury (4221), it silently calls switchChain(). A banner appears if you decline the switch.

Transaction signing

All write transactions are signed by your connected wallet provider — MetaMask, WalletConnect, etc. The app never has access to your private key. The PRIVATE_KEY env var in the deploy script is only used for server-side contract deployment, not the frontend.

Read-only mode

You can browse jobs and view job details without connecting a wallet. A wallet is only required to post jobs, take jobs, submit deliveries, evaluate, or chat.

Notification System

NOTIFICATIONS

Real-time job status alerts without WebSockets — pure polling.

Every 15 seconds, the notification system fetches get_jobs_by_client(address) and get_jobs_by_freelancer(address), then diffs the results against a snapshot stored in a React ref. Status transitions trigger notifications.

Notification Types

approved· Job transitions DELIVERED → COMPLETED

"Your delivery was AI approved! Funds released for "Job Title"."

disputed· Job transitions DELIVERED → DISPUTED

"AI disputed your delivery on Job #12. Re-evaluate?"

delivered· Job transitions ACTIVE → DELIVERED

"Freelancer submitted delivery on your Job #5."

taken· Job transitions OPEN → ACTIVE

"A freelancer accepted your Job #8."

stale· Open job older than 48 hours (client)

"Your job #3 has had no activity for 48+ hours."

Notifications persist to localStorage under the key arbiq:notifications (latest 100 kept). The first poll on page load silently seeds the state snapshot — no false positives on login.
Messaging

ON-CHAIN CHAT

Immutable client ↔ freelancer messages stored directly in the contract.

Every message is written on-chain via send_message(job_id, content). Messages are stored as a JSON array in messages: TreeMap[u256, str] keyed by job ID. Only the client and assigned freelancer can send or read messages for a given job.

json
{
  "sender":    "0xabc123...",
  "content":   "I've pushed the initial commit to the repo.",
  "role":      "freelancer",
  "timestamp": 1716000000
}
Who can messageClient and assigned freelancer only. Third parties cannot read or write messages.
When messaging opensAfter a freelancer takes the job (status moves from OPEN to ACTIVE). Messaging before that is blocked at the contract level.
Content limitMessages are truncated to 500 characters on-chain. The UI shows a warning when approaching the limit.
Polling intervalMessages refetch every 10 seconds. Optimistic messages appear immediately with a 'confirming…' spinner until the transaction finalizes.
PersistenceMessages are permanent and immutable on-chain. There is no delete or edit functionality.
Frontend Hooks

HOOKS REFERENCE

All React hooks exported from useArbiqContract.ts and the utility hooks.

Read Hooks

useGetAllJobs(){ data: Job[], isLoading, isError }

Fetches all jobs on-chain. Refetches every 15 seconds. Used by Browse Jobs and Dashboard pages.

useGetJob(id: number | undefined){ data: Job | null, isLoading, refetch }

Fetches a single job by ID. Refetches every 10 seconds. Disabled when id is undefined.

useGetMyJobs(address: string | undefined){ postedJobs: Job[], activeJobs: Job[] }

Derived hook — filters useGetAllJobs() locally by client and freelancer address. No extra contract call.

useGetJobCount(){ data: number | unknown }

Returns total job count. Refetches every 30 seconds. Used by landing page stats.

useGetMessages(jobId: number | undefined){ data: ChatMessage[], isLoading }

Fetches chat messages for a job. Refetches every 10 seconds. Returns [] if jobId is undefined.

Write Hooks

usePostJob(){ postJob(params), simulatePostJob(params), txState, reset, isLoading }

Sends a payable post_job transaction. params: { title, description, deadline, budgetEth }. budgetEth is converted to wei via parseEther(). AI evaluation timeout: 90s.

usePostJobMilestones(){ postJobMilestones(params), txState, reset, isLoading }

Payable post_job_milestones transaction. params includes milestoneTitles: string[] (2–5). Escrow is split evenly across the milestones.

useApplyToJob(){ applyToJob(jobId, note, bid?), txState, reset, isLoading }

Freelancer submits a proposal for an OPEN job. bid is optional and informational (defaults to the posted budget).

useAcceptProposal(){ acceptProposal(jobId, freelancer), txState, reset, isLoading }

Client assigns one of the applicants. The address must have applied. Transitions the job to ACTIVE.

useGetProposals()useQuery → Proposal[]

Reads the proposals submitted for a job. Refetches every 30s and after any write.

useTakeJob(){ takeJob(jobId), txState, reset, isLoading }

Direct self-assignment fallback. Fails if job is not OPEN or caller is the client. The proposals flow is preferred.

useCancelJob(){ cancelJob(jobId), txState, reset, isLoading }

Client cancels an OPEN job and reclaims the escrow.

useSubmitDelivery(){ submitDelivery(jobId, url, note), txState, reset, isLoading }

Marks a job as DELIVERED with an evidence URL and optional note. Only the assigned freelancer can call this.

useAutoEvaluate(){ autoEvaluate(jobId), txState, reset, isLoading }

Client-only. Triggers AI consensus evaluation. Uses a 300s retry timeout because LLM inference across validators takes 1–5 minutes.

useRelease(){ release(jobId), txState, reset, isLoading }

Client manually approves and pays the freelancer. Bypasses AI. Only callable by the client when status is DELIVERED.

useResubmitDelivery(){ resubmitDelivery(jobId, url, note), txState, reset, isLoading }

Freelancer resubmits on a DISPUTED job (max 2×), returning it to DELIVERED for re-evaluation.

useReclaimExpired(){ reclaimExpired(jobId), txState, reset, isLoading }

Client reclaims escrow on an ACTIVE job whose deadline passed without a delivery.

useReclaimDisputed(){ reclaimDisputed(jobId), txState, reset, isLoading }

Client reclaims escrow on a DISPUTED job after the freelancer has exhausted all resubmits.

useRateFreelancer(){ rateFreelancer(jobId, stars, review), txState, reset, isLoading }

Client rates the freelancer (1–5★) on a COMPLETED job. One rating per job.

useSetProfile(){ setProfile(name, bio, skills), txState, reset, isLoading }

Updates the connected wallet's public profile (display name, bio, skill tags).

useGetProfile()useQuery → FreelancerProfile

Reads an address's profile: reputation, completed/disputed counts, total earned, and average star rating.

useGetJobsPage()useQuery → { total, offset, limit, jobs }

Paginated, newest-first job read backed by the contract's get_jobs_page view.

useSendMessage(){ sendMessage(jobId, content), txState, reset, isLoading }

Appends a message to the job's on-chain chat thread. Content is truncated to 500 chars on-chain.

Utility Hooks

useNotifications(){ notifications, unreadCount, newToast, markAllRead, dismissToast }

Polls chain every 15s, diffs job state, fires typed notifications. Persists to localStorage. First poll is silent (no false positives on page load).

useLocalFavorites(){ favorites: Set<number>, toggle(id), isFavorite(id) }

localStorage-backed job favorites under key arbiq:favorites. Hydrated in useEffect to avoid SSR mismatch.

useScrollReveal(staggerMs = 80)RefObject<T>

Attach the returned ref to a container. All .reveal children animate in with staggered delay when they enter the viewport via IntersectionObserver.

useCountUp(target, duration = 900, trigger = true)number

Animates a number from 0 to target using easeOutExpo over duration ms. Only starts when trigger is true — pair with a scroll reveal intersection flag.

Trust Model

SECURITY MODEL

What Arbiq guarantees and what it does not.

Funds are safe from the client

Once a job is posted, the client cannot withdraw the escrow. The contract has no admin function that lets the client reclaim funds outside of the status machine.

Funds are safe from the platform

There is no admin key or owner address on the contract. No one can drain the escrow except via the defined state transitions (AI approval or manual release by client).

AI is tamper-resistant, not infallible

The AI evaluation uses gl.eq_principle.strict_eq — all validators must independently agree. A single malicious validator cannot alter the outcome. However, the AI can still misjudge a delivery based on insufficient evidence.

Messages are immutable

On-chain messages cannot be deleted or edited. Do not send sensitive personal data through the chat system.

No appeal in DISPUTED state

If AI rejects a delivery, funds remain locked. There is currently no mechanism to appeal or escalate. Write detailed job descriptions and delivery notes to reduce dispute risk.

Frontend is not the source of truth

Always verify transactions on the GenLayer Explorer. The frontend polls chain state and may lag by up to 15 seconds. Never rely solely on UI state for financial decisions.

What's Next

ROADMAP

Planned features, in-progress work, and shipped milestones.

v0.1 — Shipped

✦ Done
On-chain job posting with GEN escrow
Freelancer job discovery & acceptance
Evidence submission (URL + note)
AI consensus evaluation via GenLayer validators
Manual client release (bypass AI)
On-chain chat between client and freelancer
Real-time consensus phase tracker (PROPOSING → ACCEPTED)
Notification system (polling + localStorage persistence)
Job favorites (localStorage)
Browse & filter jobs by status, budget, keyword
Light / dark theme toggle

v0.2 — Marketplace Core (Shipped)

✦ Done
Cancel job & escrow refund — client cancels an open job, GEN returned automatically
Proposals & bidding — freelancers apply with a note/bid, client picks who gets the job
Deadline enforcement & ghost protection — client reclaims escrow if an assigned freelancer never delivers
Dispute resolution end-state — client reclaims escrow after max resubmits (no permanently-locked funds)
Partial payment support (milestone-based escrow releases)
Freelancer profiles — display name, bio & skills, set on-chain by each address
Ratings & reviews — client rates the freelancer 1–5★ with a review on completion; averaged on-chain
Freelancer reputation score (on-chain completion history)

v0.2.1 — In Progress

In Progress
Proposals, profile editor, ratings & cancel/refund UI surfaces (contract layer is live)Working
Job categories and skill-tag search/filter on-chain
Rate limiting & anti-spam at the contract level
Off-chain notifications (email / push) in addition to in-app polling

Security Hardening — Shipped

✦ Done
auto_evaluate() restricted to the client; evidence URL, note & fetched page content fenced as untrusted data with explicit anti-prompt-injection instructions
Per-job escrow tracking (escrow_remaining) so payouts can never draw from funds escrowed for other jobs
SSRF guard on /api/preview-url — blocks private/loopback/link-local/metadata IPs, resolves DNS & re-checks the resolved address, restricts to standard ports
Bounded page & limit ints (+ encoded address) on /api/explorer-txs to stop upstream query-param injection
On-chain deadline enforcement + client refund path for expired/un-taken jobs (reclaim_expired)
set_profile cannot forge reputation/earnings; client≠freelancer enforced on assignment
Paginated get_jobs_page() (newest-first, clamped limit) to avoid unbounded view gas/time at scale

v0.3 — Planned

Planned
Mainnet deployment (real GEN tokens)
Multi-deliverable jobs (accept partial evidence per milestone)
Public job feeds & embeddable job widgets
Mobile-optimised UI with WalletConnect deep links

v1.0 — Future

Future
DAO governance for AI evaluation parameters
Custom validator selection per job (specialized AI models)
Encrypted on-chain messaging (symmetric key via ECDH)
Multi-currency escrow (any ERC-20)
Cross-chain job posting (EVM ↔ GenLayer bridge)

Contributing

Arbiq is open source. Have an idea or found a bug? Open an issue or PR on GitHub ↗. Feature requests that align with the roadmap are prioritised.

READY TO BUILD?

Post your first job or start earning on Arbiq.