// teardown
The escrow payment rail
Money that waits for the work.
Charge to release
Dispute branch
a ruling releases or refunds
// overview
Escrow that protects both sides of a marketplace is only as real as where the money sits. bartr's services rail charges the customer on the platform's own Stripe balance, holds the funds while the work happens, and releases them through one function, whether the customer approves, the clock runs out, or an admin rules on a dispute.
// how it works
01 · price
Fees are frozen onto the order
Pricing resolves a fee schedule, the category's active version first and then the global default. Fees are marginal bands, so each rate applies only to the slice of price inside its band, and crossing an edge never reprices the whole job. The breakdown and the schedule version behind it are written onto the order once, so an accepted quote settles on the terms it was priced at.
02 · charge
The charge lands on the platform
The customer pays through a PaymentIntent created on bartr's own Stripe account, with no connected-account header and no application fee. The full total sits in the platform balance, where bartr can still give it back. Checkout refuses a provider who can't yet receive payouts, a transfer group links the charge to its eventual payout in Stripe's reporting, and the signed payment webhook records the charge id that every later Transfer is tied to.
03 · deliver
Approval runs on a clock
When the job is done, the provider submits proof of work and the order waits for approval with a release date seven days out. The customer can approve, which releases the funds now; request a revision, which sends the order back and takes it off the clock until new proof restarts it; or dispute. If the customer does nothing, an hourly job releases the funds once the date passes, so silence doesn't strand a provider's pay.
04 · release
One function moves the money
Approval, auto-release and a dispute ruling for the provider all call one function, and it makes exactly one Stripe Transfer out of the platform balance. The Transfer carries source_transaction, so Stripe won't send funds the original charge hasn't settled, and every path sends the same idempotency key and the same body. It refuses orders already refunded or transferred, and a Transfer it can't confirm is recorded as unconfirmed, which blocks payouts and refunds until someone reconciles it.
05 · dispute
A dispute is a status, not a filter
A customer can dispute any order that's waiting for approval, with a written reason and one open dispute per order. Raising it records the dispute, moves the order to disputed and pages staff in Slack. Nothing in the dispute code tells auto-release to skip the order: the release job only selects orders awaiting approval, so a disputed order is invisible to it by construction. Withdrawing restores the order without resetting its release date, so a dispute can't stall payment.
06 · rule
A person rules, after claiming it
No timer resolves a dispute. An admin claims it in one transaction before any money moves, so two admins can't pay the provider and refund the customer at once. The claim is a 15-minute lease, longer than the action holding it can run. The ruling is binary and needs written notes: release through the same Transfer function, or refund the whole charge, leaving the amount to Stripe's ledger. A daily job pages staff at day 3, day 7 and every day from day 14.
// decisions
The parts that were a call, not a default — and what the alternative would have cost.
Charge on the platform, transfer on release
Direct or destination charges that settle funds with the provider at checkout
Both alternatives put the money in the provider's Stripe account the moment the charge succeeds. A delayed payout schedule only postpones the bank sweep; it isn't custody. Holding the charge on the platform makes escrow a fact about where the money sits, turns a refund before release into an ordinary platform refund, and lets the payout wait as long as the work does.
One release function, one idempotency key per order
Each release path calling Stripe with its own request
Stripe replays a reused idempotency key only when the request body matches. A different body under the same key is rejected, and once the key expires a retry creates a second Transfer. So the body is built in one place with no per-path metadata, and which path released the funds is recorded in Convex instead. Guarding the choke point covers every current caller and every future one.
Disputes wait for a claimed admin ruling
Resolving disputes automatically after a timeout
Auto-releasing to the provider pays out the work the customer is contesting. Auto-refunding charges the provider for slow staffing and rewards waiting it out. Holding the funds until a person decides is never wrong on its own terms; the cost is that someone has to respond, which is what the escalation ladder is for.
Versioned marginal fee bands, frozen onto each order
Flat percentage constants in code
Bands can be retuned from the admin without a deploy, and a missing schedule throws instead of pricing at zero. Schedules are never edited in place: a change deactivates the old version and inserts the next, and each order keeps the version that priced it, so a dispute months later reconciles against the exact bands in force at checkout.
Narrow Stripe wrappers per domain
One permissive Stripe request helper
The marketplace's Stripe calls share one core for the request, error shape and audit trail, but each domain's wrapper exposes only the parameters that domain may use. The payout wrapper has no connected-account parameter at all, so a Transfer can't be sent on a provider's behalf without editing the wrapper itself. The rule is to add a wrapper, never a parameter.
// guarantees
Held, not forwarded
The customer's payment sits in the platform balance until an explicit release, so a refund before release is an ordinary platform refund, not a clawback.
Paid exactly once
Every release path shares one function, one idempotency key and one request body, and it refuses any order already refunded, transferred or unconfirmed.
Disputes wait for a person
A disputed order drops out of auto-release by status alone. Money moves only on a claimed, written admin ruling, and open disputes escalate until someone decides.