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

# Budget pacing

> W5.2 per-offer pacing multiplier that boosts behind-schedule offers and damps ahead-of-schedule ones during batch decisioning. Default off; gated by tenantSettings.aiAnalyzerSettings.ranking.budgetPacingEnabled.

## What it does

Given a per-offer **pacing plan** (expected delivery curve across some
period — e.g. 24 hourly buckets summing to 1.0) and the **delivered
history** so far, pacing computes a score multiplier per offer:

* **Behind schedule** (delivered \< ideal) → multiplier > 1, boosting
  the offer's rank so it catches up.
* **Ahead of schedule** (delivered > ideal) → multiplier \< 1, damping
  the offer so it falls back to plan.
* **On pace** → multiplier ≈ 1, no-op.

The multiplier is bounded by `[minMultiplier, maxMultiplier]` (defaults
`[0.5, 2]`) so a wildly behind offer cannot dominate the ranking and a
wildly ahead one cannot disappear entirely.

## Configuration

Two layers:

1. **Tenant flag** under `aiAnalyzerSettings.ranking`:
   ```json theme={null}
   { "ranking": { "budgetPacingEnabled": true } }
   ```
2. **Per-offer pacing config** on the Offer record under `pacing`:
   ```json theme={null}
   {
     "pacing": {
       "plan": {
         "planShape": [0.005, 0.003, /* … 24 hourly fractions … */],
         "totalBudget": 10000
       },
       "delivered": [12, 9, 8, /* … per-bucket actuals … */]
     }
   }
   ```

The plan shape can be flat (24 equal hourly fractions of the total
budget) or daytime-weighted (more spend in business hours, less
overnight). The `delivered` array is updated by your delivery
pipeline — the wire only reads it.

## When pacing applies vs no-ops

* Flag off → wire never runs.
* Flag on AND offer has well-formed `pacing.plan.planShape` → multiplier
  applied, `pacingAgg.applied++`.
* Flag on AND offer is missing `pacing` or has malformed shape →
  `pacingAgg.awaitingConfig++`. Score is preserved.

## What gets logged

Per batch run with the flag on:

```
INFO pacing.applied {
  runId, tenantId,
  applied, awaitingConfig
}
```

When the flag is off, **nothing** is logged for pacing — same volume
discipline as the Lagrangian runbook.

## How the multiplier is computed

```
factor = actualDelivered / idealDelivered     (clamped to [0.01, 100])
multiplier = (1 / factor) ^ sensitivity       (default sensitivity = 0.5)
multiplier = clamp(multiplier, minMultiplier, maxMultiplier)
```

`sensitivity` controls how aggressively pacing reacts. Default `0.5`
applies a square-root curve — a 2× under-pace doubles the score, a 4×
under-pace triples it.

## Honest limits

* **Bucket = current hour of day.** V1 hard-codes `currentBucket` to
  `now.getHours()`. Operators that need other granularities (e.g.,
  campaign-day or week-of-quarter) cannot configure that yet.
* **No automatic delivery tracking.** The `delivered` array is read
  directly from `Offer.pacing.delivered`. Operators must update that
  array themselves; there is no built-in "on impression, increment
  delivered\[currentHour]" hook in the batch pipeline yet.
* **Per-offer only.** Cross-offer / portfolio pacing is not modeled
  in V1.

## Operational checklist

* Pick `flatHourlyPlan` for fixed-rate distribution; pick
  `daytimeHourlyPlan` for traffic-weighted distribution.
* Watch `pacing.applied` per batch run. `awaitingConfig` should match
  exactly the offer count that lacks pacing config — anomalies suggest
  shape drift.
* Tune `sensitivity` per channel: high-traffic channels (e.g. push) can
  tolerate aggressive sensitivity (0.7+); low-traffic (file-delivery)
  channels prefer conservative (0.3).

## Cross-references

* [`ranking-exp3ix.mdx`](/decisioning/ranking-exp3ix)
* [`ranking-goal-seek.mdx`](/decisioning/ranking-goal-seek)
* [`ranking-lagrangian.mdx`](/decisioning/ranking-lagrangian)
