Phase 1 shipped a wallet with nothing behind it. A balance was a sum of rows any user could insert for themselves, and pressing Withdraw wrote a negative row and said it was being processed. That is now a real marketplace: the agent's card is charged, Tour Dash holds the money, and the provider is paid when the showing is confirmed done. We take 20% on a single house showing, less as the service gets more expensive.
The sequence is the design. Paying is what books the applicant, rather than something that happens after.
| Step | What happens |
|---|---|
| Provider signs up | Before anyone can book them they connect a bank account through Stripe Express. Stripe collects the identity and banking details on its own pages and runs the checks. Tour Dash stores the account id and three booleans, never an SSN or an account number. |
| Agent picks an applicant | The button says Book and pay $50 rather than Approve, so the amount is named before a card is touched. If the provider has not finished payout setup the booking is refused outright, because the money would arrive with no lawful way out. |
| Agent pays | Stripe Checkout takes the card. Nothing in the app has changed yet. |
| Stripe confirms | Only now does the webhook approve the application, create the appointment and mark the listing filled. Approving first would let a declined card leave a listing locked to a provider who never gets paid. |
| Showing happens | The money sits with the platform in between. That is deliberate: while it is held there is something to refund if the showing falls over. |
| Agent confirms it happened | Mark complete and pay transfers the provider their share. Stripe pays it out to their bank from there. |
The status dropdown no longer offers "Completed". It used to, and setting it did nothing but change a label, which after this change would have stranded the provider's money. There is now exactly one route to a completed listing and it is the one that pays.
The Settings page used to promise a 5% platform fee. That was placeholder copy from the build rather than a decision, and 5% of a $50 showing is $2.50, which does not cover the card processing on the transaction. The rate is now anchored where the volume is: 20% on a single house showing. The rule underneath is $5 plus 10% of the price, which is exactly 20% on the ordinary $50 job and slides down from there.
| Service | We keep | Provider gets | Our rate | Showami keeps |
|---|---|---|---|---|
| $45 | $9.50 | $35.50 | 21.1% | $17.00 · 37.8% |
| $50 — single house showing | $10.00 | $40.00 | 20.0% | $18.00 · 36.0% |
| $100 | $15.00 | $85.00 | 15.0% | $21.00 · 21.0% |
| $200 | $25.00 | $175.00 | 12.5% | $25.00 · 12.5% |
| $400 | $45.00 | $355.00 | 11.2% | $33.00 · 8.2% |
Part of the fee is flat, so the rate falls as the service gets more expensive, from 30% on the $25 floor to about 11% on a $400 job. That taper is the point: a $400 job is not eight times the work of a $50 showing, and a flat percentage would be heaviest exactly where two licensed agents find it easiest to settle up off the platform.
Against Showami we are well under on the ordinary showings that carry the volume, $10.00 against their $18.00 at the median, level with them around $200, and above them at the very top of their range. That is a deliberate trade rather than an oversight, and it is the number to revisit first if high-value jobs start leaving the platform.
One detail worth recording because it was nearly a bug. An earlier draft capped the fee at a flat $50, and processing on a $2,000 service is $58.30, so the cap would have meant paying $8.30 for the privilege of running the booking. The cap now cannot push the fee below processing plus a margin, and it sits high enough ($150, binding around $1,450) to be a safety rail against a mistyped price rather than a pricing lever.
Three things, each guarding a specific way a marketplace loses money.
| Guard | What it prevents |
|---|---|
| Clients cannot write money | The policy letting a user insert their own transactions is dropped. While the balance was decorative that was harmless; the moment a balance can be paid out it is a way to mint money. The webhook is now the only writer, and every table holding money is readable by the two people involved and writable by nobody. |
| Every event handled once | Stripe retries a webhook until it gets a success, and says plainly that an event can arrive twice even after one. Each event id is recorded before the work is done, so a retry cannot pay a provider a second time. The transfer itself also carries an idempotency key. |
| One copy of the fee | The fee is computed in the browser, to tell the agent what they will pay, and in the Edge Function, to tell Stripe what to take. Two hand-maintained copies is how a marketplace quotes one number and charges another, so the build now generates the second from the first and fails if they drift. |
The database is already live. The application code is committed. The functions that actually talk to Stripe are written but not deployed, so nothing can take a payment yet.
| Piece | Status | Detail |
|---|---|---|
| Database | Live | payments and stripe_events tables, Connect columns on profiles, the tightened transaction policy. Applied and verified. |
| Stripe keys | Stored | Publishable key in the app environment. Secret key held outside the repository and loaded into Supabase secrets, never compiled into the browser bundle. |
| App code | Committed | Typechecks and builds. Payout setup, the pay-to-book flow, mark-complete-and-pay, the honest wallet, the real fee table. |
| Edge Functions | Live | All five deployed and answering. Verified against the deployed bundle rather than the local file: a $50 showing returns a $10.00 fee in production. |
| Webhook endpoints | Live | Two of them. Events about our own account (a checkout completing, a refund) sign with one secret; account.updated for a provider's connected account is only ever delivered to a Connect endpoint, which signs with another. Same URL, same handler, both secrets tried. |
| Platform payouts | Blocked at Stripe | The Tour Dash account reports payouts_enabled as false. Charges work; money cannot leave until Stripe's remaining account requirements are satisfied in the dashboard. |
One thing still needs a person rather than more code: the platform account reports
payouts_enabled: false, and a provider cannot be paid out of an account that
cannot itself pay out. That is finished in the Stripe dashboard, not here.
After that, walk one real booking end to end before letting this near a median showing. The $25 minimum exists partly so that first test costs $25 rather than $50.