Price rules and margin guardrails
A price rule sets what you charge for a usage event. A margin guardrail sets the lowest margin you accept. This guide shows both, with the check() and track() results they produce.
Every amount in the examples is illustrative, and so are the providers (Vidlab, Textco) and the catalog entries shown: none is a real provider price.
Before you start
- You know the parts of pricing from cost and who controls each variable.
- You have at least one cost model, and a plan with a rate to point the rule at. Prepaid and postpaid billing covers plans.
Price rules
A price rule works out the price of a usage event from its quantity and properties. A plan rate uses it with price_rule instead of a fixed amount:
There are three methods.
cost_plus prices the cost of the usage from your cost models, then adds a markup or targets a margin.
Eight seconds of 1080p costs 8 × 12 = 96, so the price is 96 × 1.4 = 134.4 (US$1.344; rounding happens once per invoice line, as usual). Two seconds of 720p costs 12, which would price at 16.8, so the floor of 50 applies and the response says floor_applied: true. markup_percent is cost plus that share: any decimal of zero or more, so "1.5" prices at two and a half times cost. target_margin_percent makes the margin that share of the price, so "0.3" prices at cost over 0.7; it is a decimal from 0 to below 1, like margin_percent on a run. Send one of them. floor_decimal raises a price that comes out below it and cap_decimal lowers one that comes out above it, both in minor units; the response says floor_applied or cap_applied.
The basis catalog (and priced_by: catalog on a cost) is older than the provider price catalog and means priced from your cost models, whether their rates come from the provider price catalog, your overrides or your own rate cards. The event’s dimension values come from its properties, so track() carries {"resolution": "1080p"} and the rule finds the 1080p rate. The rule’s basis can also be run_cost, which prices from the costs trace() recorded under the event’s run_id. That fits pass-through pricing of tokens after the work. check() can’t know that cost in advance, so it prices from estimated_usage or from the observed average.
table looks the price up by dimension values and doesn’t touch your cost models. It suits a price list your customers see.
formula evaluates an expression over cost, quantity and numeric event properties written properties.<name>, with +, -, *, /, brackets, min(), max() and ceil(). ceil(cost * 1.25 / 10) * 10 prices at cost plus 25%, rounded up to the next 10 cents. A formula that doesn’t parse, or names something unknown, is a validation error that names the path.
Prepaid plans. The price is in money, and Runledger turns it into credits at credit_value_decimal, which is how many minor units one credit is worth. Without it, the plan’s top-up price per credit is used. To weight credits by model or by plan tier, see the recipes.
Outcomes. Price rules price usage events. An outcome charge keeps its fixed price from the plan, because its cost isn’t known when the charge is created. Margin guardrails cover outcomes too, as described next.
Currency. A rule has one currency, and it must match the currency of the rate cards of the cost models it reads and of every plan that uses it. Runledger doesn’t convert between currencies, so a rule that mixes them fails validation, and reports show each currency separately.
Try a rule before a plan uses it. POST /v1/events/preview takes price_rule and price_rule_version, and returns the pricing the rule would give an event even when no plan rate uses the rule. See Preview a price.
Deleting. A rule that a plan rate still uses can’t be deleted: DELETE /v1/price_rules/{id} returns 409 with invalid_state. Move the plan rate to another rule or a fixed amount first.
Versions. Each update to a rule makes a new version that applies to events from its effective_from. Earlier events keep the price they were given. Set effective_from in the future to give customers notice. A plan rate that uses a rule follows the version in force when each event happens. Plans with fixed rates stay as they are today.
Margin guardrails
A guardrail sets a floor on margin for some of your work, and says what happens below it.
applies_to can name an event_name, an outcome_definition and a plan, and all of them must match. With none, the guardrail covers everything.
warn(the default) adds the warningmargin_below_guardrailto thecheck()andtrack()response, and sends themargin_guardrail.breachedwebhook. A notification rule on the same event tells people.blockalso makescheck()deny the work, with the reasonmargin_below_floor.track()never blocks, because the work has already happened, so it adds the warningmargin_below_guardrail.
The two names are one guardrail seen two ways. margin_below_floor is the reason on a denied check, and appears in reason. margin_below_guardrail is the code on a warning, and appears in warnings[].code. Neither is an error code.
The basis says which margin is compared with the floor:
expectedis the margin the work should have: its price against its expected cost, worked out whencheck()runs. The cost comes from the first of three sources:estimated_usageif you send it; then, for acost_plusrule on thecatalogbasis, the rule’s own cost lines priced from the event’squantityandproperties; then the average cost of recent work of the same kind over the guardrail’swindow(defaultP7D). With none of them, the guardrail isn’t evaluated:margin.evaluatedisfalse, each guardrail’sstatusisnot_evaluated, and the response carries the warningno_cost_estimate. It doesn’t deny work it can’t judge.realizedis confirmed revenue minus cost over the trailingwindow, for the work the guardrail covers, counted the way the margin report counts it. A rising reversal rate lowers it, so a guardrail on one outcome definition can warn you when reversals start to eat the margin.
A check() that is allowed carries the margin it expected:
After the provider’s price goes up and the rule holds its prices, the same request is denied:
Blocking is a real decision: a denied check() is a customer who can’t do what they asked. Start with warn, watch the warnings for a few weeks, and move to block once the floor is one you would stand behind.
Next steps
- Cost attribution and margin sends the costs a guardrail compares against.
- Provider price changes and simulation replays recent usage against a rule or guardrail before it goes live.
- Checks and holds explains how a denied
check()reaches your code.