A campus services marketplace for international students — post a task, get bids, settle in USDC on-chain.
TaskMarket combines a structured task → bid → order → review flow with non-custodial USDC escrow on Coinbase Base, so payment is locked the moment a task is posted and auto-releases when the work is confirmed. The platform never holds user funds.
🔗 Live demo: taskmarket.pages.dev
This repository is the public overview. The application source lives in a private repo.
- What It Is
- Screenshots
- Product Mind Map
- System Architecture
- Escrow Lifecycle
- What Makes It Different
- Tech Stack
- Distinctive Features (Deep Dive)
- Use Cases
- Repository Model
- Roadmap
For international students, day-to-day help — package pickup, food runs, airport rides, tutoring, moving — gets traded in noisy group chats with no payment guarantee and weak accountability. TaskMarket turns that into a structured marketplace:
- A user posts a task with a clear price and optional USDC deposit into the escrow contract.
- Other users bid (price + ETA + pitch). The poster sees a ranked bid book.
- Poster picks a winner → order created → both sides chat in realtime.
- The taker submits completion evidence (text + up to 9 photos).
- Poster confirms → smart contract releases USDC to the taker, minus a 5% platform fee.
- If there's a disagreement, either side opens a dispute → an admin arbiter does a one-time binary call on-chain (pay taker, or refund poster).
The platform's arbiter key is the only on-chain privilege the operator holds — it can resolve disputes but cannot move funds anywhere else.
Market — ranked task feed with live bid heat, sort by price/distance, category chips, and fuzzy pinyin search
mindmap
root((TaskMarket))
Posting
Category chips
Price + location
Urgent flag (boost)
Auto-match toggle
USDC escrow opt-in
Discovery
Ranked feed
Sort price / distance
Fuzzy pinyin search
Hot tasks ranking
Top-priced ranking
Live online counter
Bidding
Price + ETA + pitch
Bid book per task
Value score
Realtime updates
Order Flow
Match
In-progress chat
Evidence form
Completion confirm
Auto-timeout release
Settlement
USDC on Base
Non-custodial escrow
5% platform fee
Auto split on release
Refund path
Disputes
Either side opens
Admin arbiter UI
One-time binary call
Gas / wallet monitor
Trust
Star ratings
Repeat-history signals
User profile
Block / report
Realtime
Socket.IO
Live bid heat
Live online counter
Conversation pin / mute / archive
flowchart LR
subgraph Client["Client · React 19 + Vite"]
A1[Landing / Market / Task Detail]
A2[Post / Bid / Orders]
A3[Messages · Socket.IO]
A4[Admin Disputes]
A5[Wallet · viem]
end
subgraph Server["Server · Node 22 + Express"]
B1[REST · auth / tasks / bids / orders]
B2[Realtime · Socket.IO]
B3[Escrow API · ethers v6]
B4[Workers]
B5[Fuzzy Search · pinyin-pro]
end
subgraph Workers["Background Workers"]
W1[escrowReleaser]
W2[gasMonitor]
W3[orderTimeouts]
W4[taskLifecycle]
end
subgraph Data["Data"]
D1[(Prisma 6)]
D2[(SQLite)]
D3[Uploads · multer]
end
subgraph Chain["Coinbase Base"]
C1[USDC ERC-20]
C2[TaskEscrow.sol]
end
A1 & A2 & A3 & A4 -->|HTTPS / WSS| B1
A3 <-->|WSS| B2
A5 -->|approve + deposit| C1
A5 -->|deposit / release / refund| C2
B3 -->|read events, resolve disputes| C2
B1 --> D1 --> D2
B1 --> D3
B4 --> W1 & W2 & W3 & W4
W1 -->|auto-release after timeout| C2
W2 -->|read arbiter balance| C2
B5 --> B1
The on-chain state machine inside TaskEscrow.sol is intentionally small — there are exactly four states and no upgrade path:
stateDiagram-v2
[*] --> None
None --> Funded: poster<br/>deposit(taskId, payee, amount)
Funded --> Funded: assignPayee(taskId, taker)<br/>(if payee was zero)
Funded --> Released: release(taskId)<br/>by poster
Funded --> Refunded: refund(taskId)<br/>by poster (if not yet released)
Funded --> Released: resolveByOwner(favorPayee=true)<br/>arbiter
Funded --> Refunded: resolveByOwner(favorPayee=false)<br/>arbiter
Released --> [*]
Refunded --> [*]
Key invariants:
- No third destination.
resolveByOwneronly routes funds to the original payer or the assigned payee. - Fee is capped. Max 20% by constant, default 5%. The owner can change fee but cannot exceed the cap.
- Each
taskIdis single-shot. Re-funding the same task ID reverts withEscrowAlreadyExists. - Self-deal blocked.
payer == payeereverts withSelfDeal.
| Most "campus task" apps | TaskMarket |
|---|---|
| Fixed-price postings | Bid book with price + ETA + pitch |
| Trust = vibes | Star ratings + repeat history + value score |
| Pay via Venmo / e-Transfer | USDC on Base locked in a non-custodial contract |
| Platform holds your money | Platform cannot move user funds; only arbiter for disputes |
| Chinese names → bad search | Pinyin fuzzy search for "weixinquanzi" → "微信圈子" |
| Group-chat threads die | Per-task Socket.IO chat + pin / mute / archive / block |
| No exit path on stale orders | Auto-release / auto-refund background workers with timeouts |
| Layer | Tech | Notes |
|---|---|---|
| Frontend | React 19, Vite 7, Tailwind CSS 4, react-router-dom 7 | Server-Components-style data fetching, modern JSX runtime |
| Wallet / chain | viem | Type-safe Ethereum client for approve, deposit, release, refund from the browser |
| UI primitives | lucide-react | Icon set |
| Realtime | socket.io-client | Bid heat, online counter, chat |
| Backend | Node.js 22, Express 4 | REST routes for auth / tasks / bids / orders / chat / escrow / admin |
| Realtime (server) | Socket.IO 4 | Per-task rooms |
| Auth | JWT + bcryptjs | Email + password |
| ORM | Prisma 6 | 13 models — User, Task, Bid, Order, Review, Favorite, Conversation, Message, ConversationPin, ConversationMute, ConversationArchive, UserBlock, MessageReport |
| DB | SQLite + Prisma | Single Prisma schema (dev & prod) |
| Files | multer | Avatar + completion-evidence uploads |
| Search | pinyin-pro | Chinese-aware fuzzy search |
| Smart contracts | Solidity 0.8.20 | TaskEscrow.sol, no external imports |
| Contract tooling | Hardhat 2.22 | Compile, test, deploy to Base / Base Sepolia |
| Chain ops (server) | ethers v6 | Read contract events, resolve disputes from server |
| Deployment | Cloudflare Pages + Cloudflare Tunnel | Frontend on Pages (taskmarket.pages.dev), backend API exposed via Tunnel; run-production.sh lifecycle |
The platform's economic core is a single-file Solidity contract (TaskEscrow.sol) deployed on Coinbase Base. It holds USDC for in-flight tasks and is the only path money can move.
- Deposit — poster calls
approve(USDC)thendeposit(taskId, payee, amount).taskIdisbytes32generated by the backend (typicallykeccak256(orderId)). - Release — when the poster confirms completion,
release(taskId)splits funds:(amount - fee)to the taker,feeto the platform treasury. - Refund — if no one takes the task, or the poster cancels before release,
refund(taskId)returns the full deposit. - Dispute —
resolveByOwner(taskId, favorPayee: bool)lets the platform arbiter pick one of the two existing outcomes. There is no third destination address, and the operator cannot change the amount.
Instead of "first taker wins," each task accumulates a bid book where each bid carries price, ETA, and a short pitch. A valueScore utility on the server combines price, taker rating, and completion history into a sortable ranking, so the poster doesn't have to manually compare ten variations of "我能做 $5."
Socket.IO drives three live surfaces:
- Bid heat on the market list — new bids land without a refresh
- Online counter — visible on the market header (e.g. "192 在线")
- Per-task chat — when a bid is matched, a conversation opens; conversations support pin / mute / archive / block / report
The server runs four small workers (server/src/utils/):
| Worker | What it does |
|---|---|
escrowReleaser |
Auto-releases on-chain escrow when both sides have confirmed but the poster forgets to click "release" |
gasMonitor |
Watches the arbiter wallet's ETH balance and warns the admin UI before disputes can't be resolved |
orderTimeouts |
Auto-cancels matched orders that the taker never starts |
taskLifecycle |
Sweeps stale tasks back to OPEN or marks them expired |
The fuzzy-search utility uses pinyin-pro so a user can type daiquhongbao and still find 代取红包. This is critical for a Chinese-language student marketplace running on a Latin-keyboard search box.
When a taker says "done," they fill in an evidence form: short description + up to 9 photos. The poster sees the same evidence inline on the orders page and can either confirm (→ escrow release) or open a dispute (→ admin queue).
The admin dispute screen surfaces the operator's real-time on-chain state in one glance: arbiter address, ETH balance, target chain ID. If gas is low, the screen visually warns before disputes start failing.
- 📦 Package pickup, parcel handoff at the front desk
- 🍱 Grocery / food / boba runs
- 🚗 Airport pickup, Union Station drop-off, local rides
- 📚 Homework help, paper proofreading, exam tutoring
- 🧹 Apartment cleaning, kitchen deep-clean
- 🪑 IKEA assembly, moving day muscle
- 🐕 Dog walks, plant watering
- 📸 LinkedIn headshots and other low-friction student gigs
| Repository | Visibility | Purpose |
|---|---|---|
taskmarket-overview |
Public | Product overview, screenshots, diagrams, roadmap |
taskmarket-source |
Private | React app, Express API, Prisma schema, Solidity contract, Hardhat deploy scripts |
The source is private because it includes deployment configuration, the production database schema, and code paths to the live escrow contract on Base.
- mainnet launch of
TaskEscrow.solon Base after audit - in-app wallet connect (Coinbase Smart Wallet) so users without MetaMask can fund tasks
- gasless deposits via paymaster / 4337 sponsor for first-time users
- multi-language UI — English + Simplified Chinese parity
- reputation rollups — verifiable on-chain rating history (Soulbound NFT) once volume justifies it
- categorized search filters beyond the current 8 chips
- task templates for repeat-poster flows (weekly grocery, recurring tutoring)
- Live demo: taskmarket.pages.dev
- Public overview: github.com/YSKM523/taskmarket-overview
- Coinbase Base: base.org
- Contact: open an issue







