# Commerce & pricing (/docs/billing/commerce)



Commerce is the **settled** stage of the clearing lifecycle for the lead economy
that ships today: what a <Term name="Worker" /> pays for the work assigned to it,
and what reverses when that work is bad. It has three APIs: **pricing** (what a
recipient pays per routed item and how much budget they have left), **returns**
(the <Term name="Warranty" /> window — send a routed decision back for a credit
and/or a reroute), and **compliance** (audit trails and regulatory reports over
everything above). A balanced-entry <Term name="Settlement" /> journal that
unifies these ships as an opt-in co-write; this page is the per-recipient pricing
and returns that clear real work now.

The auction/market **settlement** layer — bid sessions, clearing, and settlement
receipts — is documented separately at
[Routing strategies → Markets](/docs/strategies/markets). This page covers the
pricing, returns, and compliance APIs that page does *not*; the two meet only at
the [market linkage](#market--auction-linkage) section below.

<Callout title="Two ledgers, one word" type="info">
  "Ledger" appears in two unrelated places. The **pricing/settlement ledger**
  described here records per-decision charges and credits inside Ductor. The
  **wallet ledger** that moves real money lives in the
  [Stripe integration](/docs/billing/stripe). They are distinct subsystems — do
  not conflate them.
</Callout>

All examples assume local development, where request auth is disabled and the
tenant is carried by a header. For production auth see [Authentication](/docs/auth).

```bash
export BASE="http://localhost:8080"
export DUCTOR_TENANT_ID="11111111-1111-1111-1111-111111111111"
```

## Pricing [#pricing]

Pricing answers a single question at routing time: &#x2A;*what does this recipient
pay to receive this routable, and can they still afford it?** Configuration is
per-recipient (a plugin config block keyed `pricing`) with an optional
pool-level policy that decides *when* the charge lands.

Requests authorize against resource type `pricing`.

| Operation           | Method & path                                      | Notes                                               |
| ------------------- | -------------------------------------------------- | --------------------------------------------------- |
| Get pricing         | `GET /api/recipients/{recipient_id}/pricing`       | Current config                                      |
| Update pricing      | `PUT /api/recipients/{recipient_id}/pricing`       | Full replace of the config                          |
| Get budget          | `GET /api/recipients/{recipient_id}/budget`        | Budget status snapshot                              |
| Reset budget        | `POST /api/recipients/{recipient_id}/budget/reset` | Zeroes spend; admin action                          |
| List transactions   | `GET /api/recipients/{recipient_id}/transactions`  | Filters: `type`, `routable_type`, `include_summary` |
| Validate expression | `POST /api/pricing/validate`                       | CEL syntax/type check, no persistence               |

`GetTransactions` accepts `type` = `charge` | `credit` | `refund`, a
`routable_type` filter, cursor pagination, and `include_summary` to fold an
aggregate budget summary into the response.

### Configuration fields [#configuration-fields]

| Field                | Meaning                                              |
| -------------------- | ---------------------------------------------------- |
| `enabled`            | Master switch. When false, no charge is ever applied |
| `price_per_routable` | Default fixed price per routed item                  |
| `price_by_type`      | Map of routable type → price, overriding the default |
| `price_expression`   | CEL expression for dynamic pricing                   |
| `currency`           | ISO 4217 code, default `USD`                         |
| `budget`             | Maximum spend per period (`0` = unlimited)           |
| `budget_period`      | `daily` \| `weekly` \| `monthly` \| `unlimited`      |
| `budget_spent`       | Output-only — spend in the current period            |
| `budget_reset_at`    | Output-only — start of the current period            |

The budget status response (`GetBudget`) adds `budget_remaining`,
`budget_utilization` (a `0.0`–`1.0` fraction), `next_reset_at`, and a `status`
of `ok`, `warning`, or `exceeded`.

### Price resolution [#price-resolution]

The calculator resolves a price by strict priority — the first applicable rule
wins:

```mermaid
flowchart TD
  A[Calculate price] --> B{pricing enabled?}
  B -->|no| Z[price = 0, no charge]
  B -->|yes| C{price_expression set?}
  C -->|yes| D[evaluate CEL expression]
  C -->|no| E{type in price_by_type?}
  E -->|yes| F[use type price]
  E -->|no| G[use price_per_routable]
  D --> H[clamp negatives to 0]
  F --> H
  G --> H
```

So the order is **expression → type map → fixed default**. A disabled recipient,
or one with no expression and no matching type entry and no default, prices at
`0` (no charge). Negative results — including negative expression output — clamp
to `0`.

Every computed price carries a `PriceSource` recording how it was derived:

| `PriceSource` | Set when                                   |
| ------------- | ------------------------------------------ |
| `fixed`       | `price_per_routable` was used              |
| `type_map`    | A `price_by_type` entry matched            |
| `expression`  | The CEL expression produced the price      |
| `bid`         | The price was set by a winning auction bid |

### Pricing expressions [#pricing-expressions]

`price_expression` is a CEL expression that must return a number. It sees four variables — `routable`, `recipient`, `pool`, and
`now` — plus three pricing helpers: `price_tier(value, tiers)` (returns the price
for the highest threshold `<= value`), `min(a, b)`, and `max(a, b)`.

```bash
curl -s -X POST "$BASE/api/pricing/validate" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID" -H "Content-Type: application/json" \
  -d '{"expression":"routable.attributes.state == \"CA\" ? 35.0 : 25.0"}'
```

`ValidateExpression` compiles the expression and confirms it returns a numeric
type without persisting anything — use it before an `UpdatePricing` call.

### Budgets [#budgets]

A recipient can afford a price when their budget is unlimited (`budget == 0`) or
when `budget_spent + price <= budget`. Budgets reset on calendar boundaries for
the configured period — a new day, ISO week, or month — and `ResetBudget` starts
a fresh period immediately.

### Pool-level policy [#pool-level-policy]

A pool can carry its own pricing config (plugin key `pricing`) that sets the
pricing **mode** and price bounds for the whole pool:

| Field                     | Meaning                                        |
| ------------------------- | ---------------------------------------------- |
| `mode`                    | `fixed` (default) \| `expression` \| `auction` |
| `min_price` / `max_price` | Price bounds                                   |
| `currency`                | Pool currency                                  |
| `default_expression`      | Fallback pricing expression                    |

The mode controls *when* a charge is applied. In `fixed` and `expression` modes
the pool **charges on assignment**. In `auction` mode it does not — the charge is
deferred to market or ping/post close, because the final price is the winning bid
rather than a pre-set value.

### Enforcement & integration [#enforcement--integration]

Pricing plugs into routing at two points, and neither one charges money directly:

```mermaid
sequenceDiagram
  participant R as Routing engine
  participant BF as Budget filter
  participant CP as Charge-intent planner
  participant EI as Effect-intent handler
  R->>BF: Select candidates
  BF-->>R: drop recipients who can't afford the price
  R->>CP: assignment made
  CP-->>R: emit durable charge intent (if eligible)
  EI->>EI: settle the charge intent
```

* **Budget filter** (`NewBudgetFilter`) runs during `Select`. For each candidate
  with pricing enabled it computes the price and drops the recipient if they
  cannot afford it. Recipients without pricing enabled pass through untouched.
* **Charge-intent planner** (`NewChargeIntentPlanner`) runs after an assignment.
  It plans a charge **only** when the pool charges on assignment **and** the
  recipient's pricing is enabled and the computed price is positive. It never
  writes a charge — it emits a durable **effect intent** that is settled
  asynchronously by the pricing charge effect-intent handler. This split keeps
  charging crash-safe and idempotent.

Recipient pricing is read from the recipient's plugin config under the key
`pricing`.

## Returns [#returns]

A return sends a previously-routed decision back — optionally issuing a pricing
credit and/or automatically rerouting the item to a different recipient.
Requests authorize against resource type `returns`.

| Operation            | Method & path                                                                         |
| -------------------- | ------------------------------------------------------------------------------------- |
| Process return       | `POST /api/returns`                                                                   |
| Get return           | `GET /api/returns/{return_id}`                                                        |
| List returns         | `GET /api/returns`                                                                    |
| Return analytics     | `GET /api/pools/{pool_id}/returns/analytics` (period `24h` \| `7d` \| `30d` \| `90d`) |
| List reason codes    | `GET /api/returns/reason-codes`                                                       |
| Returns by recipient | `GET /api/recipients/{recipient_id}/returns`                                          |

A `ProcessReturn` request requires `decision_id` and `reason_code`; it also
accepts `reason_text`, `request_credit`, `request_reroute`, and `force` (an admin
override of the return window).

### Return flow [#return-flow]

```mermaid
flowchart TD
  A[ProcessReturn] --> B{already returned?}
  B -->|yes| R1[reject]
  B -->|no| C{decision assigned?}
  C -->|no| R2[reject]
  C -->|yes| D{pool returns enabled?}
  D -->|no| R3[reject]
  D -->|yes| E{within window or force?}
  E -->|no| R4[reject: expired]
  E -->|yes| F{reason code valid?}
  F -->|no| R5[reject]
  F -->|yes| G[create return, maybe credit, maybe reroute]
  G --> H[mark decision returned]
```

* **One return per decision.** A decision that has already been returned is
  rejected.
* **Assigned only.** The decision's outcome must be `assigned` and it must have a
  recipient.
* **Window.** The time since the decision was created must be within the pool's
  configured window, unless `force` is set.
* **Credit.** A credit is issued only when `request_credit` is set, the decision
  actually charged (`PriceCharged > 0`), the pool has `auto_credit` on, and the
  return is within the window. The credit uses the idempotency key
  `return:<decision_id>` so a crash between crediting and recording the return
  cannot double-credit.
* **Reroute.** A reroute happens only when `request_reroute` is set and the pool
  has `auto_reroute` on. It always excludes the original recipient.

Processing marks the decision returned and emits `RoutableReturned`, plus
`ReturnCreditApproved` and `ReturnRerouted` when those actions occur.

### Pool returns config [#pool-returns-config]

The pool's returns policy lives under plugin key `returns`:

| Field                  | Default   | Meaning                                               |
| ---------------------- | --------- | ----------------------------------------------------- |
| `enabled`              | `false`   | Allow returns for this pool                           |
| `window_seconds`       | `86400`   | How long after routing a return is accepted           |
| `reason_codes`         | *(empty)* | Allowed codes; empty accepts any                      |
| `auto_credit`          | `false`   | Auto-approve credits for valid returns                |
| `auto_reroute`         | `false`   | Auto-reroute returned items                           |
| `exclude_from_reroute` | `true`    | Keep the returning recipient out of reroute selection |

### Reason codes [#reason-codes]

The standard codes are `INVALID_DATA`, `DUPLICATE`, `OUT_OF_TERRITORY`,
`QUALITY_ISSUE`, `CUSTOMER_REQUEST`, `WRONG_TYPE`, `CAPACITY_EXCEEDED`, and
`OTHER`. When a pool configures its own `reason_codes` list, only those are
accepted; an empty list accepts any code.

### Analytics [#analytics]

`GetReturnAnalytics` reports `return_rate`, breakdowns `by_reason` and
`by_recipient`, `credits_issued`, and counts of returns within and outside the
window.

## Compliance [#compliance]

Compliance produces audit trails and regulatory reports over the commerce plane.
Every endpoint authorizes against resource type `compliance` with action `read`.

| Operation           | Method & path                                                                  |
| ------------------- | ------------------------------------------------------------------------------ |
| List reports        | `GET /api/reports` (type `consent` \| `financial` \| `performance` \| `audit`) |
| Get report          | `GET /api/reports/{report_id}`                                                 |
| Export CSV          | `GET /api/reports/{report_id}/csv`                                             |
| Export JSON         | `GET /api/reports/{report_id}/json`                                            |
| Decision audit      | `GET /api/reports/decisions/{decision_id}/audit`                               |
| Consent report      | `POST /api/reports/consent`                                                    |
| Financial report    | `GET /api/reports/financial`                                                   |
| Performance report  | `GET /api/reports/performance`                                                 |
| Subject data export | `GET /api/reports/subjects/{subject_id}/export`                                |

* **Consent report** requires `start_date` and `end_date`; it filters by
  `pool_id` and `regulation` (`TCPA`, `GDPR`, or `CCPA`) and can `include_exceptions`.
* **Financial report** returns `total_revenue`, `total_cost`, `total_returns`,
  and `net_revenue`, where net revenue is revenue minus returns and costs.
* **Performance report** returns `total_routed`, `total_accepted`,
  `total_returned`, `acceptance_rate`, and `average_latency_ms`.
* **Subject data export** is a GDPR data-subject access request — it bundles the
  subject's consent records, routing decisions, pricing transactions, and returns.

## Market & auction linkage [#market--auction-linkage]

When a pool prices in `auction` mode, the price is set by the winning bid rather
than by recipient config. Three seams connect the two planes:

* A winning bid price flows into pricing with `PriceSource` = `bid`.
* The market settlement receipt carries `pricing_usage_refs` linking the
  settlement back to pricing usage.
* Auction pool mode defers the charge to market close instead of applying it at
  assignment (`ChargesOnAssignment()` is false).

The durable market session, bid, and settlement plumbing lives in the routing
market service and is documented in full at
[Routing strategies → Markets](/docs/strategies/markets) — this page does not
duplicate it.

<Callout title="Settlement ledger vs. wallet ledger" type="info">
  The auction **settlement pricing ledger** described here (charges and credits on
  routing decisions) is distinct from the Stripe **wallet ledger** that moves real
  money — see [Stripe integration & invoicing](/docs/billing/stripe).
</Callout>

## Configuration quick reference [#configuration-quick-reference]

| Scope             | Plugin key | Key defaults                                                                                                      |
| ----------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- |
| Recipient pricing | `pricing`  | `enabled` false, `currency` `USD`, `budget_period` `unlimited`                                                    |
| Pool pricing      | `pricing`  | `mode` `fixed` (of `fixed` \| `expression` \| `auction`)                                                          |
| Pool returns      | `returns`  | `enabled` false, `window_seconds` `86400`, `auto_credit` false, `auto_reroute` false, `exclude_from_reroute` true |

## Related [#related]

<Cards>
  <Card title="Markets" href="/docs/strategies/markets" description="Broadcast claims and durable market settlement records." />

  <Card title="Usage metering" href="/docs/billing/usage-metering" description="The durable usage plane that records billable cost rows." />

  <Card title="Stripe integration" href="/docs/billing/stripe" description="Real money movement — wallets, charges, and invoicing." />
</Cards>
