Get in touch
All systems

// teardown

The escrow payment rail

Money that waits for the work.

private build·2026
Stripe ConnectStripe REST APIConvexTypeScriptSlack alerting

Charge to release

Quote
Platform charge
Proof of work
One Transfer
or, if disputed ↓

Dispute branch

Dispute hold
Admin ruling

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.

chose

Charge on the platform, transfer on release

not

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.

chose

One release function, one idempotency key per order

not

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.

chose

Disputes wait for a claimed admin ruling

not

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.

chose

Versioned marginal fee bands, frozen onto each order

not

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.

chose

Narrow Stripe wrappers per domain

not

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.

// by the numbers

7
days to auto-release, unless disputed
3
release paths, one Transfer function
1
Transfer idempotency key per order

What it did

bartr case study

// let's talk

Let's build something.

Have a product that needs both sharp strategy and someone who can ship it? I'd love to hear about it.

Get in touch

Command Palette

Search for a command to run...