# Yield & Quality analytics (/docs/strategies/yield-and-quality)



Two of the signals that drive [yield-optimized](/docs/strategies/scoring#yield-optimized)
routing — **yield** and **quality** — aren't magic numbers. They come from a
standalone analytics and configuration plane you can read, tune, and recompute
on its own. This page documents that plane: the `/api/yield`, `/api/quality`,
and `/api/forecast` endpoints.

These are the signals behind the **Priced** stage of the
[clearing lifecycle](/docs/clearing-layer). A dynamic floor is a computed number,
not a guess — the floor-price endpoint here is where that computation lives — and
the quality score is the axis a <Term name="Bid" /> is weighed on beyond price
alone. When the market clears work, this is the plane it reads from.

<Callout title="This is the source, not the scorer" type="info">
  [Scoring → Yield optimized](/docs/strategies/scoring#yield-optimized) explains how
  a strategy *combines* signals at route time — expected value ≈ conversion × value
  × quality × margin. This page is the **other side**: the APIs that measure and
  store those signals so the scorer has something to read. If you want to know *how
  routing uses* the numbers, read scoring; if you want to know *where the numbers
  come from*, read on.
</Callout>

| Plane                               | Base path                          | What it answers                                |
| ----------------------------------- | ---------------------------------- | ---------------------------------------------- |
| [Yield analytics](#yield-analytics) | `/api/yield/{pool_id}`             | What is this pool earning per unit, and where? |
| [Quality scores](#quality-scores)   | `/api/quality`                     | How good is each recipient, and why?           |
| [Forecast](#forecast)               | `/api/forecast/{metric}/{pool_id}` | What will revenue and volume do next?          |

## Yield analytics [#yield-analytics]

Yield is **revenue per routed unit**, sliced by segment, recipient, and time.
The nine endpoints under `/api/yield/{pool_id}` cover configuration, a live
floor price, point-in-time snapshots, and time-shaped views (series, heatmap,
forecast, period compare). All reads require `yield:read`; config writes require
`yield:write`.

| Operation           | Method & path                                |
| ------------------- | -------------------------------------------- |
| Get / update config | `GET` · `PUT /api/yield/{pool_id}/config`    |
| Resolve floor price | `GET /api/yield/{pool_id}/floor-price`       |
| Snapshot            | `GET /api/yield/{pool_id}/snapshot`          |
| Detailed snapshot   | `GET /api/yield/{pool_id}/snapshot/detailed` |
| Time series         | `GET /api/yield/{pool_id}/timeseries`        |
| Heatmap             | `GET /api/yield/{pool_id}/heatmap`           |
| Forecast            | `GET /api/yield/{pool_id}/forecast`          |
| Compare periods     | `GET /api/yield/{pool_id}/compare`           |

### Configuration [#configuration]

`GET`/`PUT /api/yield/{pool_id}/config` reads and writes the pool's yield
optimization settings.

<TypeTable
  type="{
  optimize_for: { description: &#x22;Optimization target the pool tunes toward.&#x22;, type: &#x22;enum · revenue | fill_rate | value | balanced&#x22;, default: &#x22;balanced&#x22; },
  lookback_days: { description: &#x22;How many days of history feed the analytics window.&#x22;, type: &#x22;integer&#x22; },
  floor_price: { description: &#x22;Minimum acceptable price per unit.&#x22;, type: &#x22;number&#x22; },
  ceiling_price: { description: &#x22;Maximum price per unit.&#x22;, type: &#x22;number&#x22; },
  auto_adjust: { description: &#x22;Enable automatic yield adjustments within the floor/ceiling band.&#x22;, type: &#x22;boolean&#x22; },
}"
/>

`PUT` rejects an invalid band (`floor > ceiling`) with `422`.

```bash
curl -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  http://localhost:8080/api/yield/$POOL_ID/config
```

### Floor price [#floor-price]

`GET /api/yield/{pool_id}/floor-price` resolves the **currently effective**
floor for a pool, optionally scoped to a `segment`. It returns the `floor_price`
and the `effective_at` timestamp it became active — the price no route below
which the pool will clear.

### Snapshots [#snapshots]

`GET /api/yield/{pool_id}/snapshot` returns a point-in-time roll-up — revenue,
volume, yield per unit, acceptance rate, and fill rate — optionally filtered by
`segment` and a `from`/`to` window. `GET /api/yield/{pool_id}/snapshot/detailed`
adds optional per-recipient breakdowns when you pass `include_recipients=true`.

### Time-shaped views [#time-shaped-views]

<Tabs items="[&#x22;Time series&#x22;, &#x22;Heatmap&#x22;, &#x22;Compare&#x22;]">
  <Tab value="Time series">
    `GET /api/yield/{pool_id}/timeseries` buckets yield by `granularity` (`1h`,
    `1d`, …) over a `from`/`to` window, each point carrying revenue, volume, and
    yield per unit. Use it to chart trends and feed dashboards.
  </Tab>

  <Tab value="Heatmap">
    `GET /api/yield/{pool_id}/heatmap` returns a two-dimensional grid with
    configurable `x_axis` and `y_axis` dimensions — `hour_of_day`, `day_of_week`,
    or an `attribute` — each cell carrying a value and a volume. It answers "when
    and for whom is this pool most profitable?".
  </Tab>

  <Tab value="Compare">
    `GET /api/yield/{pool_id}/compare` takes two windows
    (`period_a_start`/`end`, `period_b_start`/`end`) and returns a snapshot for each
    plus percentage changes in revenue, volume, and yield per unit — the "is it
    getting better?" query.
  </Tab>
</Tabs>

### Yield forecast [#yield-forecast]

`GET /api/yield/{pool_id}/forecast` projects revenue, volume, yield per unit,
and a `confidence` score over a `period` (`7d`, `30d`, …). This is the
yield-flavored forecast; the dedicated [Forecast API](#forecast) below exposes
the same modeling with more control over model choice and horizon.

## Quality scores [#quality-scores]

**This is where recipient quality scores come from.** A quality score is a
composite (0–1, surfaced 0–100) of six weighted dimensions computed from a
recipient's recent history. The scorer's `quality` term
([Scoring → Yield optimized](/docs/strategies/scoring#yield-optimized)) reads the
number this pipeline produces — it does not compute it.

| Operation               | Method & path                                  |
| ----------------------- | ---------------------------------------------- |
| Compute (sync)          | `POST /api/quality/compute`                    |
| Trigger compute (async) | `POST /api/quality/trigger-compute`            |
| Get / update config     | `GET` · `PUT /api/quality/config`              |
| Pool leaderboard        | `GET /api/quality/pools/{pool_id}/leaderboard` |
| Pool recipients         | `GET /api/quality/pools/{pool_id}/recipients`  |
| Recipient metrics       | `GET /api/quality/recipients/{recipient_id}`   |

### The six dimensions [#the-six-dimensions]

Each recipient's overall score is a weighted blend. The default weights (they
must sum to `1.0`, enforced with `422`) live in
`modules/services/quality/config.go`:

<TypeTable
  type="{
  conversion: { description: &#x22;Rate at which routed work converts.&#x22;, type: &#x22;weight&#x22;, default: &#x22;0.30&#x22; },
  return: { description: &#x22;Return / reversal rate (lower is better).&#x22;, type: &#x22;weight&#x22;, default: &#x22;0.20&#x22; },
  response: { description: &#x22;Responsiveness / time-to-act.&#x22;, type: &#x22;weight&#x22;, default: &#x22;0.15&#x22; },
  compliance: { description: &#x22;Adherence to policy and SLA.&#x22;, type: &#x22;weight&#x22;, default: &#x22;0.15&#x22; },
  engagement: { description: &#x22;Depth of engagement with routed work.&#x22;, type: &#x22;weight&#x22;, default: &#x22;0.10&#x22; },
  value: { description: &#x22;Economic value delivered.&#x22;, type: &#x22;weight&#x22;, default: &#x22;0.10&#x22; },
}"
/>

Scores map to **tiers** by threshold — platinum (≥ 0.90), gold (≥ 0.75), silver
(≥ 0.50), bronze (≥ 0.25), and probation below that. The config also carries a
`compute_interval_minutes` and a `lookback_days`/window over which signals are
measured.

<Callout title="Cold-start and probation" type="warn">
  Quality carries a **minimum-samples floor** and a **probation threshold** in its
  domain config (`min_samples`, `probation_threshold`, `probation_action`): a
  recipient with too little history is held at the `new`/probation tier rather than
  trusted on a lucky streak. This is the analytics-side counterpart to the
  `min_outcomes` gate on the
  [yield-optimized scorer](/docs/strategies/scoring#yield-optimized).
</Callout>

### Computing scores [#computing-scores]

Recomputation is explicit. `POST /api/quality/compute` runs a **synchronous**
recompute for an `entity_type` (e.g. `recipient`) scoped to a `pool_id` and
returns the computed scores inline. `POST /api/quality/trigger-compute` queues
an **asynchronous** job and returns a job ID; pass `force=true` to recompute even
when scores are still fresh.

```bash
curl -X POST -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{"entity_type":"recipient","pool_id":"'$POOL_ID'"}' \
  http://localhost:8080/api/quality/compute
```

### Reading scores [#reading-scores]

<Steps>
  <Step>
    **Leaderboard** — `GET /api/quality/pools/{pool_id}/leaderboard` returns
    recipients ranked by overall score, descending. The "who's best in this pool?"
    view.
  </Step>

  <Step>
    **Pool recipients** — `GET /api/quality/pools/{pool_id}/recipients` is the
    paginated list of every recipient's score in a pool, filterable by `tier`.
  </Step>

  <Step>
    **Recipient metrics** — `GET /api/quality/recipients/{recipient_id}` returns one
    recipient's score with its **dimensional breakdown** and tier — the explainable
    "why is this recipient gold?" record.
  </Step>
</Steps>

## Forecast [#forecast]

The `/api/forecast` API is standalone predictive analytics over a pool's history.
Three read endpoints (all require `forecast:read`):

| Operation        | Path                                      | Notes                                 |
| ---------------- | ----------------------------------------- | ------------------------------------- |
| Revenue forecast | `GET /api/forecast/revenue/{pool_id}`     | `days` horizon, `model` selector      |
| Volume forecast  | `GET /api/forecast/volume/{pool_id}`      | `days` horizon, `model` selector      |
| Breakpoints      | `GET /api/forecast/breakpoints/{pool_id}` | anomaly / structural-change detection |

Revenue and volume forecasts take a `days` horizon (default 30) and a `model`
(`auto`, `arima`, or `holtwinters`), returning daily points with confidence
intervals and the model's RMSE. Breakpoint detection returns timestamps with
before/after values and percentage change for each detected structural change.

<Callout title="Overlap with yield forecast" type="info">
  `GET /api/forecast/revenue/{pool_id}` and
  [`GET /api/yield/{pool_id}/forecast`](#yield-forecast) both project revenue. The
  yield endpoint is the quick, yield-shaped projection bundled with the yield
  analytics; the `/api/forecast` API is the dedicated modeling surface with
  explicit model choice, a `days` horizon, confidence intervals, RMSE, and
  breakpoint detection. Reach for `/api/forecast` when the forecast *is* the
  product; use the yield endpoint when you just want a number alongside the other
  yield stats.
</Callout>

## Related [#related]

<Cards>
  <Card title="Scoring strategies" href="/docs/strategies/scoring#yield-optimized">
    How routing combines yield, quality, and margin into one score at decide time.
  </Card>

  <Card title="Capacity marketplace" href="/docs/strategies/capacity">
    The over-subscription economics plane — trading spare capacity between recipients.
  </Card>

  <Card title="Feature snapshot" href="/docs/strategies/contracts#strategy-feature-snapshot">
    The typed facts — including quality and yield — a scorer reads at route time.
  </Card>
</Cards>
