Three new surfaces are now in the app: a board where you buy a lead, a referral pipeline that tracks a deal instead of holding the money, and a posting flow where the rule about who may be paid a percentage is on screen rather than buried in a policy document. It is a shell, which means the screens and the money arithmetic are real and the database behind them is not. This is what it does and where the seams are.
Two new items sit in the sidebar under My Listings, and a third screen is reachable from either of them. All three are agent-only, on the same reasoning that already hides My Listings from a contractor: a contractor neither buys leads nor takes referrals.
| Screen | Route | What it does |
|---|---|---|
| Buy Leads | /leads | Browse leads with the contact hidden, pay to unlock one, dispute it afterwards |
| Referrals | /referrals | Referrals you sent, ones sent to you, and an open board, each with a stage track |
| Post something | /sell | Pick what you are posting and see which kinds your account may post |
They also run inside /demo, which is the
easiest way to look at them because it needs no login. Try
localhost:8080/demo/leads when the dev server is up. The new pages issue no
Supabase request of any kind, so putting them in the demo does not weaken its isolation.
Every board in Tour Dash until now showed work you get paid to take. The lead board is the first screen where the person browsing is the one who will pay, and getting that wrong by even a glance is the kind of mistake that costs a signup. So each card carries the direction as an arrow and a phrase, "You pay $45.00" or "You're paid", rather than leaving it to be inferred from context.
The direction is deliberately not a colour. The three service families already use the whole categorical palette, red and green fail under deuteranopia, and amber is reserved for the primary action. An arrow leaving and an arrow arriving separate cleanly for everyone at no cost, which is the same reasoning that made provider work a hollow ring instead of a fourth hue.
The mechanic that makes a lead marketplace work is that you can see enough to price a lead and not enough to work it. Before payment the card carries the town, the price band, how soon they are moving, one line of motivation, where the lead came from and whether the person agreed to be contacted. The name, phone and email are the product, and they unlock on purchase.
Showing the seller's cut before the button is a decision worth defending. It is the number both sides argue about, and a marketplace that hides it earns a reputation for skimming that is very hard to shake off later.
Three trust mechanisms from the plan are on the screen and doing something. A seller carries a visible dispute rate, and one above 15% renders delisted and unbuyable rather than being quietly filtered out, so an agent can watch the market police itself. A lead with no consent record says so on the card instead of looking identical to one with a consent record. A shared lead counts down its remaining slots and stops selling at its cap.
This is the part of the plan that most needed to become a screen rather than a paragraph. A referral fee is a share of a commission. In most states it may only be paid broker to broker between licensed people, and RESPA section 8 bans fees paid for referring a settlement service. A lead sold for a flat price is a different object: it is data, the payment is not contingent on anything closing, and anyone may sell one.
So the posting flow shows all three kinds with their direction and their timing, and locks the one this account may not post. The important detail is that the blocked card does not disappear. It explains why, and it names the legal alternative in the same breath. A cold calling shop that finds no way to sell anything simply leaves.
"Pretend my licence is verified" is a toggle on that page, and it exists only so the gate can be demonstrated without a database column and a verification queue behind it. In the real thing the value comes from the profile. It is labelled as pretend rather than dressed up, because a demo control that looks like a real one is how a shell gets mistaken for a product.
The rule itself lives in one function,
postableKinds in src/lib/marketplace.ts, so nothing has to
re-derive it. A lead vendor gets leads only. An agent gets jobs and leads, and referrals once
their licence is verified.
There is no pay button on the referral screen, and that is the design rather than an omission. A referral settles out of a commission that does not exist yet, 30 to 120 days out. Stripe cannot authorise a card that long, so an escrow here would be a promise we could not keep.
What the screen holds instead is the agreement, the stage and the invoice. The percentage, the scope and an expiry date are frozen when both sides sign, the receiving agent moves the referral along as it progresses, and a closed referral offers to generate the broker to broker invoice. Progress is drawn as a five step track rather than a status word, because the question anyone actually has about a referral is whether it is still moving.
The fee is rendered from
referralFeeCents, so the number on the card is the number the invoice would
carry. It is an estimate because the commission is one, and the screen says so.
Worth being blunt about, because the screens look finished and a photograph of one in a meeting will outlive this paragraph.
Real. The screens, the interaction, and the money arithmetic. The lead
fee curve is a new function in src/lib/fees.ts sitting beside the existing job
curve rather than overloading it, and it survives the repo's own guard: a build step compares
src/lib/fees.ts against the copy the Edge Function uses and fails if they drift,
so the browser can never quote a fee that Stripe would not charge. The whole thing
typechecks and builds.
Not real. There are no tables. Leads and referrals come from
src/lib/marketplace-sample.ts, no card is charged, nothing is written, and a
refresh puts the board back. Every one of the three screens carries a preview strip saying
exactly that, which comes out the day the tables land.
Deliberately, purchases are held in memory rather than local storage. A purchase that survived a reload would start to feel like a real one.
The shell was built so that making it real is a swap rather than a rewrite.
Every screen reads its data through hooks in src/hooks/useMarketplace.ts, and
those hooks already have the shape their React Query replacements will have.
useLeadBoard() -> lead_listings + lead_purchases
useLeadContact() -> lead_contacts, behind a paid-purchase policy
useReferrals() -> referral_agreements
When those tables exist, three function bodies change and no page is touched. The contact details need their own table rather than columns on the lead, because Postgres row level security works on rows and not columns; a policy that requires a paid purchase row is four lines, while hiding columns on a shared table is not a thing you can do cleanly.
The one piece that is not a swap is the payment. The
purchase call in useLeadBoard stands in for a Stripe Checkout session that
charges the person who did NOT post the listing, which is the opposite of every charge the app
makes today. stripe-checkout still rejects any payer who is not the poster, so
that guard has to branch before any of this can take money. That remains the first real piece
of work, and it is unchanged from the plan.
lead_vendor signup, no bulk upload, and
no vendor storefront. The account type exists in the model and the gate honours it, but
Phase 2 is not built.Nothing here is committed or deployed. It runs on the dev server, and the five questions at the end of the plan are still the ones that decide what Phase 1 actually becomes, particularly whether a lead sells once or several times, which changes the schema rather than the styling.