Inspiration

I've been interested in exploring the idea of implementing a barter-based trading system using technology. I chose to tackle this problem in this hackathon for the niche - Board Games. The board-game community has been running "math trades" — multi-party swap chains — for over 20 years. The idea is simple: if A wants B's game, B wants C's game, and C wants A's game, everyone can trade in a loop and everyone wins. It solves the coincidence-of-wants problem that kills direct swaps. But the tooling hasn't changed, it's manual, plain text, no atomicity, no contention guarantee, no modern UI.

This hackathon was about databases. And a multi-party swap cycle is, at its core, a genuinely hard database problem. Every hand-off in the loop must commit together or not at all. When two loops compete for the same popular game simultaneously, exactly one must win. That felt like the right problem to build for a database hackathon.

What It Does

Boarderland is a board-game trading platform with three independent flows:

Swap Cycles — the hero feature

You list what you have and what you want. Boarderland builds a directed want-graph and runs the Top Trading Cycles algorithm — the same matching theory behind kidney-donor exchange chains (Roth & Shapley, Nobel Prize 2012) — to find closed loops of 2–5 people where everyone gives one game and receives a different one they want.

  • 2-party (bilateral) swaps: when A wants B's game and B wants A's, the system detects mutual intent and settles the trade automatically and immediately — no clicks required. Both users see the trade as completed and a chat room opens for them to arrange shipping.
  • Multi-party cycles (3–5 people): each member receives a cycle proposal showing exactly what they give and what they get. All members accept within 48 hours. When the last member accepts, the entire cycle settles in a single atomic transaction — every hand-off commits together, or none of them do. A group chat opens for all participants.

Buy / Sell — independent marketplace

Users can list games for direct sale (sell or both mode). The Buy tab shows all available sell listings from other users. Buyers make directed offers: cash, a game swap, or swap+cash. Sell-only listings are excluded from cycle matching — they participate only in this direct offer flow. First-accepted-offer wins on contention (FCFS).

Platform features: 500+ game catalog with thumbnails, condition ratings (1–5), photos/video, full trade history (Matches page), notifications, and a Mark as Failed escape hatch that re-lists games if a physical exchange falls through.


How We Built It

Stack: Next.js (App Router) on Vercel + Amazon Aurora PostgreSQL Serverless v2, built entirely with AWS Kiro.

Browser (Next.js App Router)
    ↓ Server Actions / API Routes (Node.js, Vercel)
        ↓ pg.Pool → TLS → Amazon Aurora PostgreSQL Serverless v2
            ↓ Tables: users, listings, wants, cycles, cycle_legs,
                       cycle_members, offers, chat_rooms, notifications
            ↓ Key constraint: uq_listing_consumed (partial unique index)

Matching engine runs in TypeScript on the server — pure deterministic Top Trading Cycles, no AI. It builds a want-graph, runs a bounded DFS to find cycles of length 2–5, and persists candidates. Multiple overlapping cycles sharing the same listing are all proposed simultaneously; contention is resolved at settlement time by the database.

Settlement engine uses raw pg client calls for full transaction control:

BEGIN;
  SELECT id, state FROM listings WHERE id = ANY($1) ORDER BY id FOR UPDATE;
  -- verify all available
  UPDATE cycle_legs SET consumed = true WHERE cycle_id = $1;
  -- uq_listing_consumed raises unique_violation if contested listing already consumed
  UPDATE listings SET state = 'traded' WHERE id = ANY($1);
  UPDATE cycles SET state = 'completed' WHERE id = $1;
  UPDATE cycles SET state = 'aborted'
    WHERE state IN ('proposed','accepting') AND id IN (
      SELECT cycle_id FROM cycle_legs WHERE listing_id = ANY($1)
    );
  INSERT INTO chat_rooms (context_type, context_id) VALUES ('cycle', $1);
COMMIT;

The key guarantee is one DDL line:

CREATE UNIQUE INDEX uq_listing_consumed
  ON cycle_legs (listing_id) WHERE consumed = true;

This partial unique index makes double-allocation physically unrepresentable at the storage layer. It's not an application-level check. The database makes the bad state impossible.

Challenges We Ran Into

Getting the matching filter right took several iterations.

The initial design blocked users from being in multiple proposed cycles simultaneously. That was too restrictive — it prevented the contention scenario we needed to demo, where two cycles compete for the same listing. Relaxing the filter to allow overlapping cycles (only blocking exact duplicate member sets) was the right call per the requirements, but it required careful reasoning about when the DB guarantee kicks in versus when the application layer needs to intervene.

The bilateral auto-complete introduced a subtle ordering bug.

When a reevaluate() run finds both a 2-party and a 3-way cycle using the same listing, the engine processes shorter cycles first. The bilateral would auto-settle immediately, consuming the listing, and the 3-way would then be persisted referencing an already-traded listing. The user's game disappeared from their Have list before they ever saw the 3-way proposal.

The fix was to scan all n-way candidates before processing any bilateral. If a bilateral's listing is also needed by an n-way candidate in the same run, the bilateral is deferred to proposed state instead of auto-settling. Both options appear on the user's dashboard and they choose. The database resolves whichever they accept first.

Accomplishments That We're Proud Of

The consumption guarantee is a single index. The most important correctness property in the entire platform — "a game can never be part of two completed trades" — is enforced by one DDL statement. It's not a lock, not a check constraint, not application logic. It's a partial unique index that makes the bad state unrepresentable at the storage layer. That felt like the right way to build something correctness-critical.

The matching engine handles overlapping cycles correctly. Multiple cycles can share the same listing simultaneously — that's a feature, not a bug. The engine proposes all of them. The user sees their options. The database resolves contention atomically at settlement. Getting this right required understanding exactly where application responsibility ends and database responsibility begins.


What We Learned

The database guarantee is the product's value, not its infrastructure. The platform's core promise — "you'll never give up your game and get nothing" — is literally a database constraint. That reframes how you think about schema design. The schema isn't just how you store data; it's where the correctness lives.

Ordering matters in multi-step matching. Processing shorter cycles first was the natural approach (prefer shorter cycles is a product requirement), but it created the auto-settle ordering bug. The lesson: when a processing step has side effects (auto-settlement), the order in which you process candidates becomes part of the correctness logic, not just an optimization.

What's Next for Boarderland

The engine generalizes. Board games is the beachhead. The same atomic multi-party swap infrastructure works for any high-trust barter where partial settlement would mean someone gives up their item and gets nothing: B2B equipment swapping, collectables and artifact markets, textbook exchanges, rare sports cards.

Near-term product features:

  • Real email OTP delivery (currently auto-fills in prototype mode)
  • Want-ranking + match preference filter (prefer bilateral vs shorter cycle vs best-match)
  • Demand score — "hot games" signal to help sellers price
  • WebSocket chat (currently polling-based)
  • Shipping and logistics integration for cross-border trading (mint-condition only)
  • In-platform payments via Stripe Connect for cash sales commission

Built With

Share this project:

Updates