Connectors

Action Pricing

How a connector action gets a commercial price — the two-cost invariant, tenant override precedence, the charge-policy matrix, and why a resolved price is stamped before any provider call.

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.

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.

The two-cost invariant

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

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.
Price is money
unit_price_micros on the price row — integer micros of an ISO 4217 currency. No floats anywhere in the path.
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.

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

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.

store error denied exceeded blocked succeeded provider failure replayed execution starts resolve price error — never degrades to unpriced entitlement no bill budget preview no bill quota admission no bill provider call bill resolved price no bill 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

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

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

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

Terminal outcomeDefault decisionConfigurable
successchargeyes
async_acceptchargeyes
provider_failurefreeyes
timeoutfreeyes
deniedfreeno — never billable
replayedfreeno — never billable

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

no yes store error no row row denied replayed provider failure timeout success async accept free or included charges Action executes Pricing enabled? unpriced, no bill Price row found? refuse execution unpriced, no bill Terminal outcome free, never billable free, never billable free by default free by default Tier charges? zero-cost usage event 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

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

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

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.
No environment on catalog reads
Catalog projections resolve tenant-wide rows only, so an environment override is invisible there.
No deferred async billing
Async acceptance bills or does not; deferring the decision until an async job completes is not implemented.
No RFQ price basis
rfq exists as a basis value but nothing quotes per engagement; resolution returns fixed, metered, or unpriced.

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

Where to go deeper