# Cost tools

> Reference for POST /v1/cost/analyze, which finds what a prompt wastes on prefill and how to fix it, and POST /v1/cost/estimate, which projects a monthly bill from traffic and a cache hit rate and compares it with what you pay today.

Two public endpoints answer the question prefix caching raises: what is your prompt costing you that it doesn't need to? The analyser shows where a prompt stops a prefix cache from reusing it; the estimator projects a monthly bill at a given cache hit rate. Neither needs a key, and neither stores or logs what you send.

> [!NOTE]
> Figures are at sample pricing unless you supply your own rates: Mortar's rates are derived from on-demand GPU list prices and measured throughput at 65% utilization, not quoted. Every response says so in `disclaimer`.

## Analyze a prompt

`POST /v1/cost/analyze`

Finds the values in a prompt that change from request to request and sit before content that doesn't, so a prefix cache recomputes that content every time. Each finding says how many tokens it wastes per request, what that costs and what to change. A well-ordered prompt returns no findings.

A wasted token costs what fixing it takes off the bill. On every request it's prefilled and written to the cache at the cache-write price, when it could have been read back at the cache-read price. At Mortar's sample prices that difference is 1.25 − 0.1 = 1.15 times the input price; send `pricing` to use your provider's rates instead.

The analyser has its own rate limit: 30 requests per minute per key or, without a key, per client IP address.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `prompt` | string | One of three | The text sent at the start of every request: system prompt, tool definitions, instructions. |
| `messages` | array | One of three | A conversation. Every turn but the last is treated as the reusable prefix. |
| `messages[].role` | string | Yes | Who wrote the turn. |
| `messages[].content` | string | Yes | The turn's text. |
| `variants` | array | One of three | Two to 16 prompts rendered from real requests. With these the analyser measures where requests actually differ instead of recognising values that look volatile. |
| `model` | string | No | The model whose sample price to use. Defaults to `Qwen/Qwen2.5-7B-Instruct`. |
| `requestsPerMonth` | number | No | Adds monthly totals. Leave it out and no traffic is assumed. |
| `pricing.inputPerMillion` | number | No | Your own input price per million tokens, in US dollars, used instead of the sample price. |
| `pricing.outputPerMillion` | number | No | Your own output price per million tokens. |
| `pricing.cacheReadPerMillion` | number | No | What your provider charges for an input token read from its prompt cache, per million. Leave it out and a cached token counts as free, which overstates the waste if your provider charges for one. |
| `pricing.cacheWritePerMillion` | number | No | What it charges to prefill a token and write it to the cache, per million. Leave it out and that's your input price. |

Send `prompt`, `messages` or at least two `variants`. All the text in one request, across all three, is limited to 64 KiB, about 16,000 tokens; more is refused with `413` and the code `input_too_large`.

### Example

This system prompt opens with a request ID and a timestamp, so nothing after them can be reused:

```bash title="Request"
curl https://api.mortar.one/v1/cost/analyze \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Request 3f1c9a2e-7b4d-4c1e-9a6f-0d2b8e5c7a19 at 2026-10-29T09:30:02Z. You are the support assistant for Acme Cloud. Answer questions about billing, API keys and deployments. Be concise, cite the documentation section you used, and never reveal internal ticket numbers or customer data.",
    "requestsPerMonth": 1000000
  }'
```

```json title="Response" output
{
  "object": "cost.analysis",
  "model": "Qwen/Qwen2.5-7B-Instruct",
  "basis": {
    "samples": 1,
    "method": "heuristic",
    "note": "Inferred from one prompt by recognising values that change per request. Send two or more real requests as variants for a measured result."
  },
  "prompt": {
    "totalTokens": 113,
    "cachedTokens": 2,
    "potentialCachedTokens": 65,
    "recoverableTokens": 63,
    "currentReuseRatio": 0.0177,
    "potentialReuseRatio": 0.5752
  },
  "findings": [
    {
      "id": "cache_hostile_ordering",
      "title": "A per-request value comes before stable content",
      "severity": "critical",
      "detail": "A prefix cache matches from the first token, so the first value that changes between requests invalidates everything after it. Here that is 63 tokens of otherwise identical prompt, prefilled again on every request.",
      "confidence": "heuristic",
      "evidence": [
        {
          "kind": "uuid",
          "label": "A UUID",
          "text": "3f1c9a2e-7b4d-4c1e-9a6f-0d2b8e5c7a19",
          "start": 8,
          "end": 44
        }
      ],
      "wastedTokensPerRequest": 63,
      "wastedUsdPer1kRequests": 0.0023,
      "wastedUsdPerMonth": 2.27,
      "fix": {
        "summary": "Move every per-request value after the text that never changes, for example into a final \"Request context\" section or the user message. The stable part then matches byte for byte on every request.",
        "before": "Request 3f1c9a2e-7b4d-4c1e-9a6f-0d2b8e5c7a19 at 2026-10-29T09:30:02Z. You are the support assistant for Acme Cloud. …",
        "after": "Request  at . You are the support assistant for Acme Cloud. …\n\nRequest context: 3f1c9a2e-7b4d-4c1e-9a6f-0d2b8e5c7a19; 2026-10-29T09:30:02Z"
      }
    }
  ],
  "summary": {
    "wastedTokensPerRequest": 63,
    "wastedUsdPer1kRequests": 0.0023,
    "projectedMonthlyWasteUsd": 2.27,
    "headline": "Fixing this makes 63 more tokens reusable per request: $0.0023 per 1,000 requests at $0.035995 per million, the difference between prefilling a token and reading it from the cache."
  },
  "pricing": {
    "inputPerMillion": 0.0313,
    "outputPerMillion": 0.2373,
    "cacheReadPerMillion": 0.00313,
    "cacheWritePerMillion": 0.039125
  },
  "notes": [
    "Even fixed, the reusable prefix is 65 tokens. Hosted APIs cache only a prefix above a minimum, and this one can fall below it on OpenAI and Azure OpenAI (1,024), Anthropic (512–4,096 by model), Google Gemini (2,048–4,096 by model). An engine that caches in 16-token blocks, as vLLM does, still reuses it."
  ],
  "disclaimer": "Sample pricing: Mortar's rates are derived from on-demand GPU-hour list prices and measured throughput at 65% utilisation, not quoted. See costPerMillionInput, costPerMillionOutput, costPerMillionCacheRead and costPerMillionCacheWrite on /v1/platform."
}
```

### Response

| Field | Type | Description |
| --- | --- | --- |
| `object` | string | Always `cost.analysis`. |
| `model` | string | The model whose price was used. |
| `basis.samples` | integer | How many prompts were analysed. |
| `basis.method` | string | `measured` when two or more `variants` showed where requests differ, `heuristic` when values were recognised in a single prompt. |
| `basis.note` | string | What the method means for how far to trust the findings. |
| `prompt.totalTokens` | integer | Tokens in the prompt. |
| `prompt.cachedTokens` | integer | The prefix a cache can reuse as the prompt stands. |
| `prompt.potentialCachedTokens` | integer | The prefix it could reuse with every finding fixed. |
| `prompt.recoverableTokens` | integer | The difference: what fixing the findings recovers on every request. |
| `prompt.currentReuseRatio` | number | `cachedTokens` as a share of the prompt, from 0 to 1. |
| `prompt.potentialReuseRatio` | number | `potentialCachedTokens` as a share of the prompt. |
| `findings` | array | What wastes prefill, in the order to fix it. Empty when nothing does. |
| `findings[].id` | string | `cache_hostile_ordering`: a per-request value comes before stable content. `nondeterministic_ordering`: the same JSON arrives in a different key order. |
| `findings[].title` | string | A one-line summary. |
| `findings[].severity` | string | `critical`, `high`, `medium` or `low`, by how many tokens it wastes and what share of the prompt that is. |
| `findings[].detail` | string | What the finding means for this prompt. |
| `findings[].confidence` | string | `measured` or `heuristic`, as in `basis.method`. |
| `findings[].evidence` | array | Up to 8 of the values responsible, first in the prompt first. |
| `findings[].evidence[].kind` | string | What kind of value it is, such as `uuid` or `timestamp`. |
| `findings[].evidence[].label` | string | The kind in words. |
| `findings[].evidence[].text` | string | The value as it appears in your prompt. |
| `findings[].evidence[].start` | integer | Where it starts, as a byte offset into the analysed text. |
| `findings[].evidence[].end` | integer | Where it ends. |
| `findings[].wastedTokensPerRequest` | integer | Prompt tokens this finding stops the cache from reusing. |
| `findings[].wastedUsdPer1kRequests` | number | What those tokens cost per 1,000 requests, in US dollars: each prefilled and written to the cache instead of read from it. |
| `findings[].wastedUsdPerMonth` | number | The monthly cost. Present only when you sent `requestsPerMonth`. |
| `findings[].fix.summary` | string | The change to make. |
| `findings[].fix.before` | string | An excerpt of your prompt around the first value. |
| `findings[].fix.after` | string | The same excerpt with the values moved to the end. |
| `summary.wastedTokensPerRequest` | integer | All findings together. Findings are measured in order, so theirs add up to this. |
| `summary.wastedUsdPer1kRequests` | number | The total cost per 1,000 requests. |
| `summary.projectedMonthlyWasteUsd` | number | The monthly total. Present only when you sent `requestsPerMonth`. |
| `summary.headline` | string | The result in one sentence. |
| `pricing.inputPerMillion` | number | The input price. |
| `pricing.outputPerMillion` | number | The output price. |
| `pricing.cacheReadPerMillion` | number | What a token read from the cache costs, per million. Absent when your own `pricing` left it out: a cached token then counts as free. |
| `pricing.cacheWritePerMillion` | number | What a token prefilled and written to the cache costs, per million. Absent when your own `pricing` left it out: that's then the input price. |
| `notes` | array | Caveats, such as a prefix too short for hosted APIs to cache. |
| `disclaimer` | string | Whose prices the figures use. |

## Estimate a monthly bill

`POST /v1/cost/estimate`

Projects what a workload costs a month on Mortar at the cache hit rate you give, and compares it with what you pay today. Every input is echoed back and every step of the arithmetic is written out in `math`, so you can check the result. When the inputs say Mortar costs more, the response says so.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `requestsPerMonth` | number | One of two | Requests a month. |
| `requestsPerSecond` | number | One of two | Requests a second, converted at 730 hours a month. Send this or `requestsPerMonth`, not both. |
| `inputTokensPerRequest` | integer | Yes | Prompt tokens in a typical request. |
| `outputTokensPerRequest` | integer | Yes | Completion tokens in a typical request. |
| `cacheHitRate` | number | Yes | The share of input tokens Mortar serves from the prefix cache, from 0 to 1. There's no default, because it decides the answer. |
| `model` | string | No | The model to price. Defaults to `Qwen/Qwen2.5-7B-Instruct`. |
| `baseline.label` | string | No | A name for what you pay today. |
| `baseline.inputPerMillion` | number | No | Today's input price per million tokens, in US dollars. |
| `baseline.outputPerMillion` | number | No | Today's output price per million tokens. |
| `baseline.cacheHitRate` | number | No | The share of input tokens today's provider already serves from its own prompt cache, from 0 to 1. |
| `baseline.cacheReadMultiplier` | number | With `baseline.cacheHitRate` | What a cached input token costs there, as a fraction of the input price: `0.5` is half price. |

Leave out `baseline` and the comparison is the same model with no prefix cache, which isolates what caching is worth.

Mortar's cache isn't free, and the estimate prices it as per-request [telemetry](/docs/api/telemetry) does. An input token read from the cache costs a tenth of the input price. Any other input token is prefilled and written to the cache, at 1.25 times the input price. Each model's rates are `costPerMillionCacheRead` and `costPerMillionCacheWrite` on [`GET /v1/platform`](/docs/api/platform). At a 0% cache hit rate every token pays to be written and none is read back, so Mortar costs more than the same model with no prefix cache. Against that baseline the output cancels out, and the break-even is the write premium over the read discount: (1.25 − 1) / (1.25 − 0.1), about 22%.

### Example

```bash title="Request"
curl https://api.mortar.one/v1/cost/estimate \
  -H "Content-Type: application/json" \
  -d '{
    "requestsPerMonth": 1000000,
    "inputTokensPerRequest": 2000,
    "outputTokensPerRequest": 300,
    "cacheHitRate": 0.6,
    "baseline": {
      "label": "Current provider",
      "inputPerMillion": 2.5,
      "outputPerMillion": 10,
      "cacheHitRate": 0.3,
      "cacheReadMultiplier": 0.5
    }
  }'
```

```json title="Response, abbreviated" output exact
{
  "object": "cost.estimate",
  "model": "Qwen/Qwen2.5-7B-Instruct",
  "assumptions": {
    "requestsPerMonth": 1000000,
    "requestsPerSecond": 0.3805,
    "inputTokensPerRequest": 2000,
    "outputTokensPerRequest": 300,
    "cacheHitRate": 0.6,
    "hoursPerMonth": 730,
    "inputTokensPerMonth": 2000000000,
    "outputTokensPerMonth": 300000000
  },
  "mortar": {
    "label": "Mortar",
    "pricing": {
      "inputPerMillion": 0.0313,
      "outputPerMillion": 0.2373,
      "cacheReadPerMillion": 0.00313,
      "cacheWritePerMillion": 0.039125
    },
    "cacheHitRate": 0.6,
    "cacheReadMultiplier": 0.1,
    "cacheWriteMultiplier": 1.25,
    "breakdown": {
      "uncachedInputTokens": 800000000,
      "cachedInputTokens": 1200000000,
      "outputTokens": 300000000,
      "uncachedInputUsd": 31.3,
      "cachedInputUsd": 3.76,
      "outputUsd": 71.19,
      "monthlyUsd": 106.25,
      "perRequestUsd": 0.000106,
      "blendedPerMillionTokensUsd": 0.0462
    }
  },
  "savings": {
    "monthlyUsd": 7143.75,
    "annualUsd": 85725.05,
    "percent": 98.53,
    "cheaper": true,
    "verdict": "Mortar is $7,143.75 per month cheaper (98.5%) at these inputs."
  },
  "breakEven": {
    "cacheHitRate": 0,
    "reachable": true,
    "verdict": "Mortar costs no more than the baseline even with no cache hits."
  },
  "math": [
    "requests/month = 1,000,000 (given)",
    "Mortar uncached input = 2,000,000,000 × (1 − 0.6) = 800,000,000 × $0.0313/M × 1.25 = $31.30, prefilled and written to the cache",
    "Mortar cached input = 2,000,000,000 × 0.6 = 1,200,000,000 × $0.0313/M × 0.1 = $3.76"
  ],
  "notes": [
    "Mortar bills an input token read from the prefix cache at 0.1× the input price and one prefilled and written to it at 1.25×. Output tokens are always billed: decoding cannot be cached."
  ],
  "disclaimer": "Sample pricing: Mortar's rates are derived from on-demand GPU-hour list prices and measured throughput at 65% utilisation, not quoted. See costPerMillionInput, costPerMillionOutput, costPerMillionCacheRead and costPerMillionCacheWrite on /v1/platform."
}
```

### Response

| Field | Type | Description |
| --- | --- | --- |
| `object` | string | Always `cost.estimate`. |
| `model` | string | The model priced. |
| `assumptions` | object | Every input the arithmetic used, including the derived `requestsPerSecond`, `hoursPerMonth`, `inputTokensPerMonth` and `outputTokensPerMonth`. |
| `baseline` | object | What you pay today, itemised the same way as `mortar`. |
| `mortar` | object | Mortar at your cache hit rate. |
| `mortar.label` | string | The side's name. |
| `mortar.pricing.inputPerMillion` | number | The input price used. |
| `mortar.pricing.outputPerMillion` | number | The output price used. |
| `mortar.pricing.cacheReadPerMillion` | number | What an input token read from the prefix cache costs, per million. Mortar only: the model's `costPerMillionCacheRead`. |
| `mortar.pricing.cacheWritePerMillion` | number | What an input token prefilled and written to the cache costs, per million. Mortar only: the model's `costPerMillionCacheWrite`. |
| `mortar.cacheHitRate` | number | The cache hit rate applied. |
| `mortar.cacheReadMultiplier` | number | What a cached input token costs, as a multiple of the input price: `0.1` for Mortar. On `baseline` it's the `baseline.cacheReadMultiplier` you sent. |
| `mortar.cacheWriteMultiplier` | number | What an uncached input token costs, as a multiple of the input price: `1.25` for Mortar, which writes every token it prefills to the cache, and `1` for `baseline`, which is charged no write premium. |
| `mortar.breakdown.uncachedInputTokens` | number | Input tokens prefilled each month. On Mortar each is also written to the cache. |
| `mortar.breakdown.cachedInputTokens` | number | Input tokens served from the cache each month. |
| `mortar.breakdown.outputTokens` | number | Output tokens each month. |
| `mortar.breakdown.uncachedInputUsd` | number | What the uncached input costs: at the cache-write price on Mortar. |
| `mortar.breakdown.cachedInputUsd` | number | What the cached input costs, at the cache-read price. |
| `mortar.breakdown.outputUsd` | number | What the output costs. |
| `mortar.breakdown.monthlyUsd` | number | The monthly total. |
| `mortar.breakdown.perRequestUsd` | number | The cost of one request. |
| `mortar.breakdown.blendedPerMillionTokensUsd` | number | Total spend over every token handled, cached or not: the figure to compare across vendors. |
| `savings.monthlyUsd` | number | Baseline minus Mortar, per month. Negative when Mortar costs more. |
| `savings.annualUsd` | number | The same, per year. |
| `savings.percent` | number | The saving as a share of the baseline's monthly cost. |
| `savings.cheaper` | boolean | Whether Mortar costs less at these inputs. |
| `savings.verdict` | string | The comparison in one sentence. |
| `breakEven.cacheHitRate` | number or null | The Mortar cache hit rate at which both cost the same, or `null` when caching can't close the gap. |
| `breakEven.reachable` | boolean | Whether a hit rate between 0 and 1 reaches it. |
| `breakEven.verdict` | string | The break-even point in one sentence. |
| `math` | array | The calculation, one line per step. |
| `notes` | array | The assumptions behind the figures. |
| `disclaimer` | string | Whose prices the figures use. |

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | | A required field is missing or out of range, such as `cacheHitRate` outside 0 to 1, or both `requestsPerMonth` and `requestsPerSecond`. The message names the field. |
| 404 | `model_not_found` | `model` isn't in the catalog. |
| 413 | `input_too_large` | The analyser was sent more than 64 KiB of text. |
| 429 | | More than 30 analyses in a minute. |

```bash title="An estimate without a cache hit rate" status=400
curl https://api.mortar.one/v1/cost/estimate \
  -H "Content-Type: application/json" \
  -d '{"requestsPerMonth": 1000000, "inputTokensPerRequest": 2000, "outputTokensPerRequest": 300}'
```

```json title="Response" output
{
  "error": {
    "message": "`cacheHitRate` is required: the share of input tokens served from the prefix cache, between 0 and 1.",
    "type": "invalid_request_error"
  }
}
```
