# Action Pricing (/docs/connectors/action-pricing)



An action's **quota cost** and its **price** are different things, and Ductor
keeps them apart on purpose. Quota cost is capacity: how much of a provider's
rate limit one call consumes. Price is money: what the tenant is charged for
that call. They live in different tables, use different types, and never convert
into each other.

This page describes the pricing engine that resolves what an action costs and
decides whether an execution bills.

<Callout type="info" title="Ships as an opt-in surface">
  Set `connector_pricing.enabled: true` to construct the price resolver and wire
  it into every connector action executor. While disabled — the default — no
  resolver is built, no pricing query is issued, and every action resolves as
  *unpriced*, so nothing bills.

  Enabling it makes actions billable **only for tenants with price rows on file**.
  An action with no price row stays unpriced, which is not the same as free. If
  the pricing store is unreachable, executions are refused rather than run
  unpriced, and the reason is recorded on the execution log.
</Callout>

## The two-cost invariant [#the-two-cost-invariant]

One call consumes two independent budgets, and confusing them is the failure this
design exists to prevent.

<FactGrid>
  <Fact label="Quota cost is capacity">
    `CostUnits`

     on the action's quota declaration — how much of the provider's rate limit this call spends. An integer count of calls, never money.
  </Fact>

  <Fact label="Price is money">
    `unit_price_micros`

     on the price row — integer micros of an ISO 4217 currency. No floats anywhere in the path.
  </Fact>

  <Fact label="They never convert">
    Separate tables, separate types, and lexically distinct metadata keys (

    `pricing_*`

     versus 

    `quota_*`

    ), so no call site can quietly read one as the other.
  </Fact>
</FactGrid>

A priced action with no quota declaration still spends one quota unit; a
zero-price action still consumes its declared capacity. Neither axis infers the
other.

## Resolving a price [#resolving-a-price]

A price is resolved **before any provider I/O** and stamped onto the execution.
That ordering is the point: the terminal usage event bills the price that was in
force when the call started, not whatever is current when it finishes.

```mermaid
flowchart TD
  s([execution starts]) --> r[resolve price]
  r -->|store error| err([error — never degrades to unpriced])
  r --> e{entitlement}
  e -->|denied| free1([no bill])
  e --> b{budget preview}
  b -->|exceeded| free2([no bill])
  b --> q{quota admission}
  q -->|blocked| free3([no bill])
  q --> x[provider call]
  x -->|succeeded| bill([bill resolved price])
  x -->|provider failure| free4([no bill])
  x -->|replayed| free5([no bill])
```

Notice how many paths end without billing. Denied and replayed executions are
never billable regardless of what the policy says, and a store failure surfaces
as an error rather than silently resolving to unpriced — a transient outage can
never turn paid actions into free ones.

### Precedence [#precedence]

Resolution picks one row by walking three tie-breaks in order:

1. **Tenant override beats platform default.** A row scoped to the tenant wins
   over the platform-wide listing.
2. **Environment-specific beats tenant-wide.** Within tenant scope, a row naming
   an environment beats one that leaves it blank.
3. **Highest version wins.** Among rows still in force, the greatest
   `pricing_version` is selected.

Effective windows make versions abut cleanly: `effective_from` is inclusive and
`effective_to` is exclusive, so consecutive versions meet without overlapping and
without leaving a gap.

### Unpriced is not free [#unpriced-is-not-free]

When no active row exists, the action resolves as **unpriced** — its commercial
status is unknown. That is not the same as free: only the `free` tier is free.
Catalog surfaces must render this as "Unpriced", never as `$0`.

## The charge-policy matrix [#the-charge-policy-matrix]

A price declares what happens for each terminal outcome. Defaults bill on
success and on async acceptance, and stay free everywhere else.

| Terminal outcome   | Default decision | Configurable            |
| ------------------ | ---------------- | ----------------------- |
| `success`          | **charge**       | yes                     |
| `async_accept`     | **charge**       | yes                     |
| `provider_failure` | free             | yes                     |
| `timeout`          | free             | yes                     |
| `denied`           | free             | **no — never billable** |
| `replayed`         | free             | **no — never billable** |

Notice how many routes end without a charge — that asymmetry is the design.

```mermaid
flowchart TD
    A[Action executes] --> B{Pricing enabled?}
    B -- no --> N1[unpriced, no bill]
    B -- yes --> C{Price row found?}
    C -- store error --> N2[refuse execution]
    C -- no row --> N3[unpriced, no bill]
    C -- row --> D{Terminal outcome}
    D -- denied --> N4[free, never billable]
    D -- replayed --> N5[free, never billable]
    D -- provider failure --> N6[free by default]
    D -- timeout --> N7[free by default]
    D -- success --> E{Tier charges?}
    D -- async accept --> E
    E -- free or included --> N8[zero-cost usage event]
    E -- charges --> F[bill at stamped price]
```

Two safeguards are deliberate. `denied` and `replayed` are hard-coded unbillable
regardless of policy, so a throttled, circuit-broken, auth-rejected, or replayed
mutation can never produce a charge. And an *unrecognized* policy value resolves
to free rather than to charge — an unknown policy can under-bill, but it can
never over-bill.

Tiers also gate billing: only tiers that charge per unit produce a non-zero
amount. The `free` and `included` tiers still emit a zero-cost usage event so
volume stays observable.

## Why the price is stamped, not looked up later [#why-the-price-is-stamped-not-looked-up-later]

The resolved price and its `pricing_version` are copied onto the execution and
carried into the terminal usage event. Billing therefore reads the price that
applied at execution time.

This is what makes historical bills immune to later price changes. Re-pricing an
action tomorrow cannot restate what yesterday's calls cost, because yesterday's
usage events carry yesterday's price and version — nothing recomputes from the
current table.

## Environment overrides read differently on the catalog [#environment-overrides-read-differently-on-the-catalog]

A price row can be scoped to an environment, and **execution honours that scope**:
the executor resolves with the request's real environment, so the charge is the
environment-specific one.

The catalog does not. A catalog read has no environment on its request context,
so it resolves tenant-wide rows only. A tenant that has set an environment
override therefore **sees the tenant-wide price in the catalog while execution
bills the environment-specific one**. Treat catalog price as the tenant-wide
default rather than a per-environment quote until an environment reaches the read
path.

## What is not built yet [#what-is-not-built-yet]

<FactGrid>
  <Fact label="No seller payout">
    A capped agent draw now settles to the balanced ledger as showback (tenant wallet debited, platform credited), but an action price carries no payee, so a settled purchase credits the platform, never a marketplace seller.
  </Fact>

  <Fact label="No environment on catalog reads">
    Catalog projections resolve tenant-wide rows only, so an environment override is invisible there.
  </Fact>

  <Fact label="No deferred async billing">
    Async acceptance bills or does not; deferring the decision until an async job completes is not implemented.
  </Fact>

  <Fact label="No RFQ price basis">
    `rfq`

     exists as a basis value but nothing quotes per engagement; resolution returns fixed, metered, or unpriced.
  </Fact>
</FactGrid>

A <Term name="Worker" /> drawing against a budget to buy an action now settles
that spend to the balanced ledger as showback. A catalog that pays a *seller*
for the bought action — an action price with a payee — remains
<Status kind="roadmap" />.

## Where to go deeper [#where-to-go-deeper]

<Cards>
  <Card title="Policies, Quotas & Audit" href="/docs/connectors/policies-quotas-audit">
    The capacity axis — egress policy, tenant policy, rate limits, and the audit chain.
  </Card>

  <Card title="Usage & Metering" href="/docs/billing/usage-metering">
    The metered surfaces a resolved price is recorded against.
  </Card>

  <Card title="Catalog" href="/docs/connectors/catalog">
    Parity tiers and coverage — an implementation signal, not a commercial one.
  </Card>
</Cards>
