Last week I published the complete source code of a services marketplace I built and deployed — a finished product, live at skilllanka.com, with payments, video, escrow and admin tooling all wired up. Not a tutorial project, not a starter template. The actual thing, minus the users I never had time to go and get.
It's called SkillHub on GitHub, it's MIT licensed, and you can clone it, rebrand it in two config files, and run your own marketplace with it.
Repo: https://github.com/Sameera-MHK/service_marketplace
This post is about why I did that, and a walkthrough of the one piece of it I'm most proud of: the trust score.
What it is
A two-sided marketplace for local services — think plumbers, tutors, designers, trainers — with a business directory bolted on. Four roles:
- Clients browse verified professionals, post jobs, book paid consultations, join live video classes, buy from a Pro's storefront, rate work, raise disputes.
- Pros onboard with ID verification, get a scored public profile, receive leads, sell consultations and classes, run a shop, request payouts, subscribe to a plan.
- Businesses register a company profile with services, photos and opening hours.
- Admins moderate, verify IDs, resolve disputes, approve payouts, set commissions, and read an audit log.
Under the hood: escrow-style job flow (30% deposit, balance released on client confirmation), a commission engine with per-category and per-Pro overrides, Free/Pro/Elite subscription tiers, LiveKit video, Stripe, i18n with a data-driven locale registry, and server-rendered Open Graph tags so links preview properly.
Stack is React 18 + Vite + Tailwind on the front, Node + Express + Mongoose on the back. 21 models, 17 route modules, and every third-party integration is optional — without Stripe the payment endpoints return a clear error, without Cloudinary uploads go to local disk, without SMTP emails print to the console. You can run the whole thing with just MongoDB and two JWT secrets.
Why open-source a production app?
About a year ago I had an idea: a services marketplace for the Sri Lankan market. Sri Lanka has a huge pool of skilled tradespeople and freelancers, and no good platform connecting them to clients with any kind of trust layer. I designed it, built it, and deployed it at skilllanka.com — it's still up if you want to click around.
Then reality. I run a SaaS company, a social media agency, and a steady flow of client development work. A marketplace doesn't grow by existing — it grows by someone promoting it every day, onboarding Pros one by one, answering support, posting on social media, chasing the first hundred users. I never had the time to be that person. The code was finished and deployed. The business side never started — no marketing, no Pro onboarding drive, and so no real users. A marketplace with zero users is just a very complete demo.
I could have let it sit in a private repo. But a complete, working marketplace is worth far more in someone else's hands than it is idle in mine. Maybe someone in Sri Lanka, or Kenya, or the Philippines, or a small town anywhere, wants to be a marketplace entrepreneur and just needs the product part already done. That's who this is for.
One honest note on how it was built: the idea, the architecture, the data model, the trust score design and the business logic are mine. For the actual coding I worked with AI assistants throughout — this is a lot of code for one person, and I'd rather say so than pretend otherwise. It's deployed and running, every flow is exercised end to end with the seeded data, and Stripe runs in test mode. What it hasn't had is real traffic — that's the part I'm handing over.
Before releasing it I spent a couple of weeks doing what most open-source dumps skip:
- Stripped every brand, region, currency, price and legal line into two config files driven by environment variables
- Wrote real docs — configuration, architecture, API, deployment, and a guide to hosting a demo
- Added a seed script that creates 20 Pros, 6 businesses, 5 clients and jobs in every state, so you can see the whole app working in five minutes
- Wrote a SECURITY.md that says plainly what the code does and doesn't protect against
And then I put a note at the top of the README: shared as-is, not actively maintained. Fork it, make it yours, don't expect me to answer issues. I'd rather be upfront than let people wait on PRs that won't get merged.
The trust score: how to rank professionals without getting gamed
Every marketplace has the same problem. You need to rank providers, and the obvious signal — average star rating — is terrible.
A Pro with three 5-star reviews from their friends shows 5.0. A Pro with 200 real jobs averaging 4.6 shows 4.6. The new guy wins. Your best professional loses.
So the score in SkillHub is a 0–100 composite of five signals, recomputed on every job event:
| Signal | Weight | What it measures |
|---|---|---|
| Completion | 30% | Jobs completed vs cancelled by the Pro |
| Rating | 25% | Client ratings — Bayesian-adjusted and time-decayed |
| Responsiveness | 20% | Average response time to leads |
| Disputes | 15% | Disputes lost to the client, as a rate |
| Trust signals | 10% | ID verified, trade certification, referred by a high-score Pro |
The bands: Elite 90+, Trusted 75–89, Rising 55–74, Probation 35–54, Suspended ≤34.
Most of those are simple ratios. The rating signal is where the real work is.
Fixing the "three friends" problem: Bayesian shrinkage
Instead of trusting a Pro's raw average, we pull it toward the platform average until they've earned enough reviews to stand on their own.
const PLATFORM_MEAN_RATING = 4.2; // what a typical Pro on the platform scores
const BAYESIAN_THRESHOLD = 30; // how many reviews before we mostly trust yours
const v = ratedJobs.length;
const bayesianAdj =
(v * weightedMean + BAYESIAN_THRESHOLD * PLATFORM_MEAN_RATING) / (v + BAYESIAN_THRESHOLD);
This is the same formula IMDb uses for its Top 250. Run the numbers:
- 3 reviews, all 5 stars: (3 × 5.0 + 30 × 4.2) / 33 = 4.27
- 200 reviews averaging 4.6: (200 × 4.6 + 30 × 4.2) / 230 = 4.55
The experienced Pro wins. The new Pro isn't punished — 4.27 is above the platform average — they just haven't proved anything yet. Get to 30 reviews and your own average carries about half the weight; at 100+ it's nearly all yours.
Fixing the "coasting" problem: time decay
The second problem: a Pro who was excellent two years ago and mediocre since. A plain average barely moves.
So each rating is weighted by how old it is:
const DECAY_LAMBDA = 0.003; // per day
const weights = ratedJobs.map((j) => Math.exp(-DECAY_LAMBDA * daysAgo(j.createdAt)));
const weightedMean =
ratedJobs.reduce((sum, j, i) => sum + j.workerRating * weights[i], 0) /
weights.reduce((a, b) => a + b, 0);
At λ = 0.003, a rating's weight halves roughly every 230 days. A one-year-old review counts for about a third of a fresh one. A three-year-old review is nearly noise. Recent behaviour dominates, but history isn't erased.
Then the adjusted rating on a 1–5 scale is mapped to 0–100:
ratingScore = Math.round(((bayesianAdj - 1) / 4) * 100);
The parts that aren't math: caps and hard overrides
Weighted averages are fair, but a marketplace also needs rules that can't be averaged away.
// No ID verification? You can't score above 70, no matter how good your numbers are.
if (!user.idVerified) skillScore = Math.min(skillScore, 70);
// Lose two disputes in 30 days and you're suspended, full stop.
if (recentDisputesLost >= 2) {
profile.isSuspended = true;
skillScore = Math.min(skillScore, 34);
}
// Fraud flag from the moderation service: score goes to zero.
if (profile.fraudFlag) { skillScore = 0; scoreBand = 'suspended'; }
The ID cap is the one I'd defend hardest. It means "Trusted" and "Elite" on this platform always means a real, verified human. You can't buy your way there with reviews.
What I got wrong the first time
The first version used a plain average and a simple job count. Even with seeded test data the failure mode was obvious: new Pros with a handful of perfect reviews sitting above people with dozens of jobs. The composite score with shrinkage and decay was the second attempt, and it's the one that shipped.
The full function is about 120 lines: server/services/scoreService.js. Every constant is at the top. If your platform's ratings skew higher or lower than 4.2, change one number.
If you want to use it
git clone https://github.com/Sameera-MHK/service_marketplace skillhub
cd skillhub/server && npm install && cp .env.example .env
# set MONGO_URI, JWT_SECRET, JWT_REFRESH_SECRET
npm run dev
cd ../client && npm install && cp .env.example .env.local && npm run dev
cd ../server && npm run reseed
Log in as admin@skillhub.example.com / Admin123! and you've got a fully populated marketplace on localhost. Rebranding is client/src/config/site.js and server/config/site.js. Deployment guide, API reference and architecture docs are in /docs.
Things to know before you ship it:
- The legal pages are sample text. Get a lawyer.
- The bundled photos are placeholders. Replace them.
- Change every seeded password.
- Read SECURITY.md. It tells you what's your job.
What's actually in it that took the longest
For anyone building a marketplace from scratch, the features that ate the most time — and that you get for free here:
- The commission engine. Platform default → per-category rate → per-Pro override → volume tiers, resolved in priority order per transaction type. Every marketplace gets this wrong the first time.
- Escrow job flow with disputes. Deposit, hold, release on confirmation, admin resolution path. The state machine has more edges than you'd think.
-
Graceful degradation. Making every integration optional without
if (stripe)scattered through 50 files. It's a service layer with no-op fallbacks, and it's what makes the repo usable by a stranger in five minutes. - Trust score. Above.
If you fork it and build something, I'd like to see it. And if you've solved the provider-ranking problem differently — a different prior, a different decay, something learned — tell me in the comments. I'm sure this isn't the last word on it.

Top comments (0)