Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Forecasting spend

A banded month-end projection on the seasonal frame the detector uses, and the fallback it drops to rather than band a thin history.

ExplanationLast updated

On this page

Somebody needs a number for the month. Fifteen days in, you have spent $95 and the obvious answer is $190, which assumes the second half of a month looks like the first, that weekends count the same as Tuesdays, and that a single line is a fair summary of a thing that varies.

TokenTriage forecasts on the same seasonal frame the anomaly detector uses, and reports a band rather than a line. The interesting engineering here is not the projection. It is what happens when the history is too thin to support one.

Forecasting is part of analytics and is off by default.

A point is a range

Every series entry is three numbers, not one:

{
  "series": [
    { "ts": "2026-08-10T11:00:00.000Z", "p10": 0.002853908626941494, "p50": 0.03703725, "p90": 0.11708063420609244 },
    { "ts": "2026-08-10T12:00:00.000Z", "p10": 0.0026229359950841816, "p50": 0.03403975, "p90": 0.10760506026275801 }
  ],
  "method": "seasonal-quantile-v1",
  "insufficient": false
}

The p50 is the seasonal point: the median of that same hour of the week over the trailing four weeks. The p10 and p90 are the band around it. A single line for a quantity that genuinely varies is a claim the data cannot support, so it is not offered.

Month-end is actual-to-date plus the sum of the remaining point forecasts, and it arrives as an assessed figure with a confidence interval attached:

{
  "month_end": {
    "usd": 95.27025408,
    "verdict": "ESTIMATED",
    "basis": { "method": "seasonal-quantile-v1", "rows": 320 },
    "baseline": { "kind": "projected-spend", "desc": "projected from your recorded traffic patterns; not a savings claim" },
    "ci": { "lo_usd": 0, "hi_usd": 212.33142020445464, "level": 0.8 }
  }
}

Note the baseline. projected-spend is the one baseline kind in the registry that is explicitly not a savings claim, and the sentence beside the dollar says so. A forecast is a projection of spend rather than of a saving, and it cannot be PROVEN: it is ESTIMATED by construction, because a forecast is a model of traffic that has not happened.

Those figures are from a sandbox against mock upstreams, so the amounts are small; the shape is what a production ledger produces.

How the band is built

The obvious way to band a seasonal forecast is to take residual quantiles per hour-of-week. With four weeks of history that gives each hour-of-week a pool of about four numbers, and the 10th percentile of four numbers is the smallest of them.

That band would be “the worst of the last four Tuesdays at 9am to the best of them”. It is the observed range rather than an interval, it gets narrower as history gets thinner, and it would be exceeded about a fifth of the time while claiming 80% coverage.

So residuals are pooled across every hour in the trailing window, after being scale- normalised, each residual divided by its own point and floored at a cent. Normalising is what makes pooling legitimate: a $40 hour and a $0.40 hour have wildly different absolute residuals and comparable relative ones. The pool is then hundreds of samples instead of four, and its P10 and P90 mean something.

The floor exists for a specific failure: a relative residual against a near-zero point is unbounded, and one $2 hour where the seasonal median was a tenth of a cent would blow the band out for every hour in the horizon.

Not enough history is a state, not a guess

The banded method needs 4 populated weeks. Below that it does not narrow its band and it does not quietly extrapolate. It declines, and a labelled linear fallback answers instead.

{
  "month_end": {
    "usd": 0.003094686616443937,
    "verdict": "ESTIMATED",
    "basis": { "method": "linear-burn-v1", "rows": 1 },
    "baseline": { "kind": "projected-spend", "desc": "projected from your recorded traffic patterns; not a savings claim" }
  },
  "method": "linear-burn-v1",
  "insufficient": true,
  "insufficient_reason": "1 populated day of history (1 week populated); seasonal-quantile-v1 needs 4 populated weeks to band a forecast",
  "burn": { "usd_per_hour": 1.6553571428571427e-06 }
}

That is a genuinely new deployment (one day of history, six requests) answering the question anyway, with a burn rate, a named method, and a sentence saying what it would need to do better.

A point forecast also needs at least 2 populated same-hour-of-week samples. A median of one sample is that sample, which is a week-over-week carry-forward wearing a seasonal method’s name.

Priced spend only

A forecast covers spend the ledger could price. Rows it could not are excluded rather than folded in at zero, and their trend is disclosed alongside rather than merged:

{
  "unpriced_note": {
    "rows_per_day": 0.9285714285714286,
    "trend": "flat",
    "note": "this forecast covers PRICED spend; these rows could not be priced and are excluded, not counted as $0"
  }
}

Read the trend. A flat unpriced rate means your forecast’s coverage is stable and the projection is as good as its method. A rising unpriced rate means the forecast is covering a shrinking share of your actual traffic, and the projection is drifting low for a reason that has nothing to do with the forecasting method.

What can go wrong

You quote the p50 as the forecast. It is the middle of a band that was published because the middle alone is not the answer. Quote the range, or quote the month-end figure with its interval.

You compare a forecast to an invoice. The forecast covers priced spend; the invoice covers everything. If your unpriced row count is non-trivial, they are measuring different populations. Reconciliation is the surface for that comparison.

The band looks implausibly wide in week five. Four populated weeks is the minimum for the banded method rather than a comfortable amount for it. The interval will tighten as the residual pool grows.

Why the same seasonal frame as the detector

Both the forecaster and the anomaly detector build a same-hour-of-week baseline over a trailing four weeks. That is not a coincidence of implementation. It means a spike the detector flags and an hour the forecaster expected are computed against the same notion of normal.

The two answer opposite questions with one model of your traffic: the detector asks whether an hour that already happened broke the pattern, and the forecaster asks what the pattern says about an hour that has not.