Matching Algorithm Spec
Why not just "newest / highest value / closest value"
A naive matching algorithm (sort by recency, or by absolute/closest price) does not scale past a small catalog and produces poor matches once cross-merchant volume grows. TBBN instead computes a Trade Compatibility Score per candidate pair and ranks by that.
Trade Compatibility Score
score = 0.35 * wantsMatch
+ 0.20 * categoryMatch
+ 0.15 * brandMatch
+ 0.10 * conditionMatch
+ 0.10 * valueMatch
+ 0.05 * geographyMatch
+ 0.05 * reputationMatch
Each sub-score is normalized to [0, 100]; the weighted sum is the final score out of 100.
- wantsMatch (35%) — the dominant signal. Compares listing B's
{category, subcategory, brand}against listing A'slisting_wantsrows (and vice versa for a symmetric two-way score). An exact category+subcategory+brand hit on a declared want scores 100; a category-only hit scores partial credit (~40); no overlap scores 0. This is what turns TBBN from a search engine into a matching engine (seedocs/business/glossary.md— "Wants"). - categoryMatch (20%) — top-level category equality between what's offered and what's wanted, independent of the explicit wants array (covers sellers who browse without having filled in wants).
- brandMatch (15%) — brand equality/similarity.
- conditionMatch (10%) — condition compatibility (e.g. both "Used - Good" scores higher than "New" vs. "Used - Fair").
- valueMatch (10%) — how close the two
TradeValues are; smaller absolute/relative difference scores higher, since a near-even trade requires less settlement friction. - geographyMatch (5%) — proximity/same-country bonus, relevant for anything where shipping
cost materially affects the trade's attractiveness (deliberately low weight — TBBN is
cross-border by design, see
db-erd.md's globalization requirements). - reputationMatch (5%) — the offering seller's
reputation_score(fromreputation-service, Phase 12 — until that service is built this factor defaults to a neutral 50 for every seller, not 0, so early-network sellers aren't penalized for the absence of reputation data).
Worked example (from source spec)
Seller A wants: Apple Watch (category: Electronics, brand: Apple). Seller B has: Apple Watch (category: Electronics, brand: Apple, condition: Used - Good).
wantsMatch = 100 (exact category+brand hit) · categoryMatch = 100 · brandMatch = 100 ·
conditionMatch ≈ 90 · valueMatch depends on the two listings' TradeValue closeness (assume
~85) · geographyMatch and reputationMatch assume neutral (~50 each absent better data).
score = 0.35*100 + 0.20*100 + 0.15*100 + 0.10*90 + 0.10*85 + 0.05*50 + 0.05*50
= 35 + 20 + 15 + 9 + 8.5 + 2.5 + 2.5
= 92.5
Close to the 96 figure cited in source material for a similarly strong match — small
differences come from the assumed condition/value/geography/reputation inputs in this
illustrative example, not from a different formula.
Implementation note
Implement the scorer as a pure function in packages/core-matching (scoreTradeCompatibility( listingA, listingB, context) => { score, breakdown }) before wiring it into a live
recommendation API — this lets the algorithm be validated against worked examples like the one
above independent of infrastructure concerns, per docs/phase-plan/roadmap.md Phase 7 guidance.
Trade Value formula
TradeValue = OriginalPrice + RequestedAmount − WillingToPay
OriginalPrice is always merchant-supplied (TBBN never sets or discounts prices).
RequestedAmount and WillingToPay are mutually exclusive per listing (see
docs/business/conflict-resolution-log.md item 7) — at most one is non-zero.
Example A — Item A $500, Item B $450 → B pays $50 (the difference). Example B — Item A $500, Item B $600 → A receives $100. Example C (multi-item) — Item A $500 vs. Items B totaling $600 (200+150+250) → A pays $0 (B's side already exceeds A's side; no additional payment required from A, and the excess is simply the cost of B choosing to offer more value, not something TBBN redistributes).
Roadmap-only: ring trades / multi-hop trades
Documented, not built. Example: A wants a camera, B wants a guitar, C wants a laptop; A has a
guitar, B has a laptop, C has a camera. No pairwise match exists, but the cycle A→C→B→A
satisfies everyone. This requires a graph-search matching layer on top of the pairwise
Trade Compatibility Score and is explicitly Phase 21 — the intended long-term moat, not a
near-term deliverable. packages/core-matching's stub interface should not preclude this (keep
the scorer pairwise-composable), but no ring-trade logic exists in this scaffold.