Skip to main content
This page follows a commission from the moment a brand funds a campaign to the moment a creator withdraws it, and covers what happens when a sale is refunded or disputed.
No money is ever on-chain. Funds move through Stripe. Balances live in an internal double-entry ledger and escrow tables in PostgreSQL. The smart contract only records attribution events, including a “settled” flag that means vested, not paid. See Smart contracts.
For the user-facing explanation, see Budget, fees and refunds, Earnings and payouts and Holding periods and disputes.

The life of a commission

1. Access and funding

Access. When billing enforcement is switched on (it is off by default), before a brand on Shopify can create or fund a campaign, its store needs an active plan through Shopify’s app pricing, or the brand can hold an active FREEQ stake instead. This is an access check only; the plan is not the campaign budget. Admins are never gated, and providers without app billing are exempt. Funding. A brand funds a campaign through Stripe Checkout (POST /api/payments/campaign/{campaignId}/fund). When Stripe confirms payment, the ledger records a CAMPAIGN_FUND entry from the platform’s Stripe holding account into the campaign’s budget account, and the campaign can activate. Some partner programs that pay creators in their own points are funded notionally instead, from a separate issuance account, so points never read as cash.

2. Commission and escrow

When an order is attributed and its conversion is recorded on-chain (see Attribution engine):
  • Commission = campaign rate x the order total. This holds for every campaign, single-product ones included: once the order contains the campaign product, the whole order counts (after discounts, including tax and shipping). The base is frozen on the order before anything is sent.
  • Advertiser sales fee (Shopify stores only; Rush, Fern and fixture stores are exempt) = ADVERTISER_SALES_FEE_BPS x the same order total, rounded half-up in the order currency’s minor units. It is charged only when the order is in the campaign’s booking currency, and never on a legacy matched-lines order. It is taken from the campaign budget on top of the commission (ADVERTISER_FEE_EARNED, budget → platform:advertiser-fees:<CCY>), in the same transaction as the hold, and the rate and amount are stamped on the hold.
  • Fee first. Under the campaign row lock: if the remaining budget cannot cover the whole fee plus at least one minor unit of commission, the order books neither and ends BUDGET_EXHAUSTED. Otherwise the fee is booked in full and the commission is capped by what remains, so the last sale before exhaustion may earn a partial commission. A card-funded campaign cannot be topped up while it is live; only notionally funded partner campaigns accept top-ups.
  • The commission moves from the campaign budget into the creator’s per-campaign escrow account (ESCROW_HOLD), and an escrow hold row records when it may be released.
  • Budget-exhaustion auto-end (Shopify campaigns only). When a booking takes the budget to zero, or an order cannot be booked for lack of budget (which can leave a small residual that the brand can reclaim later), the campaign is marked end_reason = BUDGET_EXHAUSTED and ended through the normal durable end path (the manual-end:<id> task and its on-chain end). The END flip queues the campaign-ended emails to the brand and its creators. An hourly sweep re-queues a lost end, and (when BUDGET_EXHAUSTION_SWEEP_ENABLED is on) ends older campaigns already at zero.

Hold duration

Each hold is assigned a risk tier. The hold lasts max(tier days, tenant floor) from the moment it was created: Tier inputs include Shopify’s order risk assessment, platform and creator risk signals, device matching, and manual review. When the store’s risk assessment is still pending, the tier is assigned later and release waits for it. The risk engine and device matching run in shadow mode by default: they compute tiers and auto-pause decisions and write an audit record, but do not change the hold or pause the creator. Device matching is also disabled by default. With enforcement off, holds stay at the CLEAR baseline (30 days, or the tenant’s minimum hold if that is longer); dispute freezes and manual admin pauses always apply. A hold is released only when all are true: its release time has passed, its tier is assigned, it is not frozen by an open dispute, and the creator’s escrow is not paused (by an admin or by automatic fraud checks).
A campaign’s on-chain returnWindow is a different clock. It only gates the on-chain return call. Nothing in escrow, the ledger or settlement reads it.

3. Release, fees and the reserve

A scheduler checks for matured holds every minute. Releasing one hold is a single database transaction that:
  1. Moves the net amount from escrow to the creator’s available balance (ESCROW_RELEASE).
  2. Deducts any fees stamped on the hold when it was created (PLATFORM_FEE_EARNED, and PARTNER_FEE_EARNED for a partner tenant that takes a share of its creators’ earnings). Stamping at hold time means a later rate change never touches existing holds.
  3. Skims a reserve. A share of the net (20% by default) goes into the creator’s reserve pot for that campaign, until the pot reaches its cap: max(2 x the creator's average net commission on that campaign, a per-currency floor), with the floor anchored at $50 for USD.
  4. Enqueues the on-chain settlement record (below).
The reserve is a buffer that covers chargebacks arriving after release. A creator with an active FREEQ stake can claim reserve balances early, up to a ceiling derived from the stake’s value; if a stake-backed reserve is later hit by a chargeback, the loss is recovered from the stake.

4. Settlement delivery

Release never waits on the chain. A separate worker drains the settlement queue and calls registerSettlement on the registry for each released hold, with leases, bounded retries and strict sequencing. On-chain, settlement flips isSettled and emits ConversionSettled. It means the commission vested. It does not move money.

5. Refunds, partial refunds and chargebacks

A partial refund reduces the held commission in proportion to the order value refunded, whichever items were returned. The reduction comes out of the commission held at order time, so a later rate change does not affect it. Open disputes freeze the hold; a won dispute unfreezes it. Chargebacks are risk signals. Refunds inside the hold are not: the commission is simply reversed. Refunds after release are a realised loss, so un-waived post-release refunds feed the creator’s risk score and the late-loss guardrail.
Known gap (UAM-974). A refund is normally sized from the payments Shopify lists in the refund webhook. If a refund arrives with no payments listed, we fall back to the refunded items’ prices, which leave out tax and shipping. Commission is a percentage of the order total including tax and shipping, so in that case too little commission is taken back, and a full refund may not be recognised as full. It is not yet known when Shopify sends such a refund (store credit and restock-only refunds are candidates). Accepted for now; revisit if one is seen on real traffic.

6. Creator withdrawal

Creators are paid through Stripe Connect Express accounts.
  1. The creator onboards (POST /api/payments/publishers/{publisherId}/stripe/onboard) and can open Stripe’s dashboard through a login link.
  2. The creator withdraws (POST /api/payments/publishers/{publisherId}/withdraw). This pays the full available balance as one Stripe transfer per campaign with a balance. Only the creator can withdraw; admins cannot withdraw on their behalf.
  3. A per-creator lock prevents two withdrawals at once (409 while one is in flight; the second request is refused, not queued, and should be retried after the first completes). The ledger records PAYOUT_INITIATED and PAYOUT_COMPLETED.
There is no on-chain withdrawal.

7. Reclaiming unused budget

When a campaign completes, the brand can reclaim what is left (POST /api/campaigns/{id}/withdraw-funds). The platform issues a Stripe refund against the original charge and returns PENDING; the refund is confirmed asynchronously by Stripe’s webhook, which records CAMPAIGN_REFUND. The call is safe to repeat. If the original charge can no longer be refunded, a platform administrator settles the remainder out of band so the budget is zeroed and recorded.

The ledger

The ledger (LedgerEntry, table ledger_entries) is the record of truth for money. Accounts are strings with a fixed naming scheme: Invariants:
  • Double entry. Each event is one row with a debit account, a credit account and one amount. The amount leaves one and enters the other.
  • Append-only. Rows are never updated or deleted. A correction is a new reversing row.
  • Idempotent. Every row has a unique idempotency key; repeating a write returns the existing row.
  • Integer minor units. Amounts are integers in the currency’s smallest unit, with an explicit currency code.
  • Balances are sums. A balance is the sum of credits minus debits for an account. Some tables carry cached aggregate columns; for creator money, always derive from the ledger.
The entry types are the LedgerEntryType enum in services/prisma/schema.prisma.

Brand spend

A campaign’s spend is derived from the ledger, not from budgetAmount − budgetRemaining (a reclaim zeroes the cached remaining budget, which would read as everything spent). From every row whose debit or credit account is exactly campaign:<campaignId>:budget:
  • spent = CAMPAIGN_FUND credits − CAMPAIGN_REFUND debits − the budget account’s balance
  • platformFee = ADVERTISER_FEE_EARNED − ADVERTISER_FEE_REVERSED
  • commissions = spent − platformFee
The campaign owner (or an admin) sees this as metrics.spendBreakdown on GET /api/campaigns/{id} and GET /api/campaigns/{id}/metrics, and as spent / commissions / platformFee on GET /api/payments/campaign/{campaignId}/balance. It is null (fail closed) when a row is in another currency than the booking currency, a ledger type or direction not expected on a budget account appears, or a total goes negative. GET /api/pricing?storeId= returns the current fee rate for a store the calling brand is a member of.