> ## Documentation Index
> Fetch the complete documentation index at: https://docs.higherfrequency.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Money path

> 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...

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.

> [!NOTE]
>
> **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](https://docs.higherfrequency.io/developers/smart-contracts.md).

For the user-facing explanation, see [Budget, fees and refunds](https://docs.higherfrequency.io/brands/budget-fees-and-refunds.md), [Earnings and payouts](https://docs.higherfrequency.io/creators/earnings-and-payouts.md) and [Holding periods and disputes](https://docs.higherfrequency.io/how-it-works/holding-periods-and-disputes.md).

## The life of a commission

```
Brand ──Stripe Checkout──► campaign budget ──advertiser fee──► platform:advertiser-fees
                                 │  order attributed + recorded on-chain    (Shopify stores)
                                 ▼
                         creator escrow (held)
                                 │  hold expires, no pause, no open dispute
                                 ▼
             fees ◄──── release ────► reserve skim ──► creator reserve
                                 │
                                 ▼
                       creator available balance
                                 │  creator withdraws
                                 ▼
                    Stripe Connect transfer ──► bank
```

## 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](https://docs.higherfrequency.io/developers/attribution-engine.md)):

* **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 | Days |
| - | - |
| `CLEAR` (default) | 30 |
| `WATCH` | 45 |
| `T1` | 60 |
| `T2` | 90 |
| `T3` | 120 |

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).

> [!WARNING]
>
> 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

| Event | Hold still in escrow | Hold already released | On-chain |
| - | - | - | - |
| Full refund | Hold cancelled; commission back to the campaign budget (`REFUND_REVERSAL`), advertiser fee back too (`ADVERTISER_FEE_REVERSED`) | No clawback from the creator; the commission stays spent from the brand's campaign budget. The advertiser fee is kept | `registerReturn` |
| Partial refund | Hold reduced in place by the refunded share; the rest keeps maturing. The advertiser fee is reduced by the same share (`ADVERTISER_FEE_REVERSED`) | As above | `registerPartialReturn` |
| Chargeback (dispute lost) | Hold cancelled; commission back to the budget (`CHARGEBACK_REFUND`), advertiser fee back too (`ADVERTISER_FEE_REVERSED`) | Debited from the creator's reserve (`CHARGEBACK_DEBIT`), or recovered from a stake-backed reserve's stake (`STAKE_SLASH_RECOVERY`); any shortfall is recorded as unrecovered. The advertiser fee is kept | `registerChargeBack` |

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.

> [!WARNING]
>
> **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:

| Account | Holds |
| - | - |
| `platform:stripe:holding` | Funds in the platform's Stripe balance |
| `campaign:<campaignId>:budget` | A campaign's remaining budget |
| `publisher:<id>:escrow:<campaignId>` | A creator's held commission for one campaign |
| `publisher:<id>:reserve:<campaignId>` | A creator's reserve for one campaign |
| `publisher:<id>:available` | Released funds, ready to withdraw |
| `platform:fees:<currency>` | Platform fees skimmed from creator commission at release |
| `platform:advertiser-fees:<CCY>` | Advertiser sales fees taken from campaign budgets at conversion (net of `ADVERTISER_FEE_REVERSED`) |
| `tenant:<slug>:fees:<currency>` | A partner tenant's fees |

**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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.