Forj
A freelance marketplace built around on-chain escrow, where agreements and payments are governed by smart contracts.
- Category
- Web3 Freelance Marketplace
- Year
- 2026
- Status
- Testnet
- Technologies
- Solidity, Base, Smart Wallets, Next.js, TypeScript, tRPC, Drizzle, PostgreSQL, Node.js
- Index
- 01 / 05

01Overview
Forj is a freelance marketplace where the agreement and the payment are governed by a smart contract rather than by platform policy. It is a personal project, built solo and currently running on Base Sepolia. The application handles matching, proposals, messaging and reviews off-chain; the money is held and released by an escrow contract that either party can read.
02Problem
Freelance transactions depend on both parties trusting the platform to manage payments and resolve disputes. That creates a layer of dependency between client and freelancer around exactly the moments that matter — funding, milestones, and settlement. The rule that decides who gets paid is a policy document rather than something either side can inspect.
03Approach
Treat escrow as a state machine, not a service. The release conditions are written into a contract neither party controls, and the platform's job narrows to the parts software is genuinely better at — finding people, describing work, and showing the state of an agreement clearly. This is an exploration of that model rather than a claim that it is the right answer for every case; it trades dispute flexibility for legibility, and the arbiter path exists precisely because that trade is not free.
04System
Two sources of truth, split on purpose. Postgres owns identity and content — users, jobs, proposals, messages, reviews. Base owns money: every escrow row in the database has a matching entry on-chain, and the row is reconciled against the chain on every state-changing call. The database is treated as a cache that can always be re-derived, so a disagreement between the two is resolved in the chain's favour. A single escrow service is the only code permitted to talk to the chain, and the server pulls transaction receipts itself rather than trusting what the client reports.
- 01
Client
- Next.js · React 19apps/web — RSC pages plus the dashboard shell. The only consumer of every other workspace package.
- PrivySign-in by email, Google or wallet. Issues the session cookie and provisions an embedded wallet and a smart wallet.
- wagmi · viemChain reads and writes from the browser, connected through Privy.
- 02
Edge
- /api/trpcThe tRPC route handler. Verifies the Privy token, resolves the Postgres user row, and becomes the auth boundary for everything below.
- webhooks · uploads · jobs/api/webhooks/privy for user lifecycle, /api/uploadthing for file callbacks, /api/inngest for background jobs.
- 03
Domain
- tRPC routersEleven routers — user, job, proposal, contract, message, review, notification, service, savedJob, search, admin — rate-limited through Upstash Redis.
- escrow serviceThe only code that talks to the chain. Validates each transition, submits it, verifies the receipt and event payload, then updates the database.
- PimlicoBundler and paymaster. Batches approve + fund into one sponsored UserOperation, so funding costs the client no gas and one confirmation.
- 04
State
- Neon Postgres · DrizzleSource of truth for identity and content. Holds a mirror of each escrow that is reconciled against the chain on every state change.
- Base Sepolia · ForjEscrowSource of truth for money. USDC held by ForjEscrowV3 — OpenZeppelin Ownable2Step, ReentrancyGuard, Pausable, SafeERC20. Pausing blocks new funding and nothing else: settlement, refunds, submissions and disputes always work.
- Resend · UploadThingTransactional email rendered with React Email, and file storage for avatars and attachments.
- FundedDeposit held, waiting for the first delivery.
- SubmittedDelivery under client review.
- RevisionRequestedClient asked for changes, up to the revision cap.
- DisputedFrozen. Only the arbiter or the timeout settles it.
- ReleasedterminalPaid to the freelancer.
- RefundedterminalFull deposit back to the client. No fee kept.
- ResolvedterminalDispute settled by split.
Transitions
- FundedSubmitted
submitWork() - SubmittedRevisionRequested
requestRevision() - RevisionRequestedSubmitted
submitWork() - SubmittedReleased
release() · releaseAfterReview() - FundedRefunded
cancelByFreelancer() · refundAfterDeadline() - SubmittedDisputed
raiseDispute() - DisputedResolved
resolveDispute(shareBps) — arbiter, before deadline - DisputedResolved
resolveExpiredDispute() — anyone, 50/50, no fee
ForjEscrowV3. Every terminal state is reachable without the arbiter — the dispute path has a deadline, and anyone can settle an expired one.
05Interface
The interface is built so that the chain is visible without being the subject. A contract reads as an ordinary project view — what was agreed, what is owed, what happens next — and the on-chain timeline sits alongside it as evidence rather than as the main event. Funding is a single confirmation with no gas prompt, because the approve and fund calls are batched into one sponsored operation.



06Current Status
Live on Base Sepolia as a public testnet, running ForjEscrowV3 — a fresh deployment rather than an upgrade, with v2 left immutable beside it. The full lifecycle is exercised end to end: fund, submit, revise, release, and both dispute paths. The Base mainnet entry in the contract registry is still an empty string, so nothing is on mainnet, nothing is audited, and there are no users beyond my own test wallets. Milestones are tracked off-chain, so the contract still holds and releases the full amount in one transaction however a job is broken up.
07Links
- View live project
- Source is a private repository