agenthropic

Cost model

This page covers how agenthropic turns raw token counts into trustworthy dollar figures: the token_usage buckets that each carry their own per-token rate, the dated model_pricing table those buckets are priced against, the compaction-baseline repricing that keeps historical totals correct after a context rewrite, the delegation-savings metric that re-prices Haiku/Sonnet routing at top-tier rates to quantify what routing actually saved, and the CI rule that turns pricing staleness into a build failure instead of a silent wrong number. The key takeaway: every dollar this system ever displays traces to a single, auditable equation — ground-truth tokens × a dated, versioned price — never a runtime estimate, never a hardcoded constant nobody re-verified (the “cost-trust chain,” concept-analysis-v2 §3, H-COST). Cost correctness is treated as one of the four real testable units of this project, alongside ingest, tree, and live-flow correctness (concept-analysis-v2 §4.3).

Update — 2026-07 (as built). The cost engine is real: pure functions in packages/core/src/cost (compute-cost.ts, compaction-repricing.ts, delegation-savings.ts), the pricing table in migration 7, and a served read surface (GET /api/sessions/:id/cost-analysis, GET /api/cost/summary). The equation held — every dollar is tokens × a dated price — but four details landed differently than this page sketches:

In one picture

~/.claude/projects/*.jsonl                 model_pricing (dated, versioned)
  (ground-truth token counts)                effective_from / verified_on
        │                                           │
        ▼                                           ▼
   token_usage                              PricingProvider
   bucketed by:                             (timestamp-aware
   speed / inference_geo /                   dated-price resolver)
   service_tier,                                    │
   compaction baseline preserved,                    │
   agent_id nullable → backfilled                    │
        │                                            │
        └──────────────────┬─────────────────────────┘
                            ▼
                        CostEngine
              cost = Σ (tokens_in_bucket × dated_rate_for_bucket)
                            │
              ┌─────────────┴─────────────┐
              ▼                           ▼
    per-session / per-agent      delegation-savings
      dollar cost                (actual model vs.
                                  top-tier re-price)
              │                           │
              └─────────────┬─────────────┘
                             ▼
                  Cost query API → cost/Sankey view

This is the same hook-ingest → SQLite (WAL) → read API shape described in the architecture overview; the cost model is the pricing layer bolted onto the token_usage projection, not a separate pipeline.

(As built, the picture’s shape holds with two label changes: the bucketing column reads input / output / cache_read / cache_write_5m / cache_write_1h rather than speed/geo/tier, and model_pricing carries effective_from only — no verified_on. agent_id is attributed before the write — subagent rows by the parser’s hard join, main-transcript rows by the writer, which resolves the parser’s null to the session id; migration 8 back-fills only rows persisted before that resolution existed.)

1. Ground truth in, dollars out

Every figure on this page starts from the same non-negotiable invariant that governs the rest of the system: token counts are read from ~/.claude/projects/*.jsonl, never inferred (DESIGN §3; concept-analysis-v2 CD-3). The cost model does not introduce a second, cheaper source of token counts — it consumes the exact same ground-truth numbers the subagent tree and the live status view consume, and adds exactly one thing on top: a price.

concept-analysis-v2 states this as a named crosscut, the cost-trust chain (§3, H-COST, CD-3 + CD-4):

every displayed dollar traces to (ground-truth tokens × a dated, priced model); a model observed in fixtures with no price row FAILS CI — silent staleness is a red build, not a runtime “estimated” label. This extends the byte-exact-tokens guarantee to the priced output.

The risk this guards against is named explicitly in the source analysis: the token_usage/compaction-baseline graft is “the most subtle piece of the entire costing model” — getting it wrong “means silently wrong dollar figures — which undermines the moat’s credibility” (concept-analysis.md §3.3). The negative-test catalogue backs this with a dedicated regression: “compaction-mid-session and PreCompact re-pricing” is one of the two v1-only cases added on top of the ten EXPANDED §7.1 negative scenarios (concept-analysis-v2 §6, Tree correctness). The rest of this page is the mechanism that closes that gap: buckets that track what actually changes the rate, a pricing table that is dated rather than a single constant, and a compaction baseline so a context rewrite mid-session doesn’t corrupt history.

2. token_usage: bucketed by whatever changes the rate

The base schema (simple10-derived: projects/sessions/agents/events/filters) does not model cost at fine grain. token_usage is a graft from hoangsonww, adopted specifically because its bucketing is production-grade (DESIGN §4):

token_usage bucketed by speed / inference_geo / service_tier (each changes the per-token rate), preserving compaction baselines so historical totals still price correctly after a context rewrite. Production-grade costing.

Bucket dimension Why it exists
speed Throughput/latency characteristic of the request — carries its own rate.
inference_geo Regional routing of the inference call — carries its own rate.
service_tier The API service tier the call was served under — carries its own rate.

As built, the table above is design history. The real corpus’s usage blocks do not carry speed/inference_geo/service_tier at all — the dimensions that actually change the rate in the transcripts are the token buckets themselves: input, output, cache_read, cache_write_5m, cache_write_1h. Migration 6 (apps/server/src/db/migrations.ts) stores one row per (message_id, bucket) — long format, UNIQUE (message_id, bucket), because naive row summation over-counts by ~2.4–2.7× — a PROVISIONAL single-corpus range, not a constant (parser-spec §5.2; earlier revisions of this page quoted only the bottom of it as “roughly 2.4×”):

CREATE TABLE token_usage (
  id                      INTEGER PRIMARY KEY,
  session_id              TEXT NOT NULL,
  agent_id                TEXT,
  message_id              TEXT NOT NULL,
  model                   TEXT NOT NULL,
  bucket                  TEXT NOT NULL CHECK (bucket IN ('input','output','cache_read','cache_write_5m','cache_write_1h')),
  tokens                  INTEGER NOT NULL,
  is_compaction_baseline  INTEGER NOT NULL DEFAULT 0,
  occurred_at             TEXT,
  UNIQUE (message_id, bucket)
);

tokens is copied verbatim from JSONL — the never-inferred rule shipped intact. agent_id is nullable. Subagent rows are attributed inside the parser via the hard join (parser-spec §5.1), before any row is written; main-transcript rows arrive from the parser with a null agent id and the writer (db/token-usage.ts) resolves them to the session id — the main agent’s node id — at insert time. Migration 8 back-fills that same resolution into rows persisted before it existed. A NULL that survives both means genuinely unattributable.

One column in that DDL does nothing, and is documented rather than quietly implied to work: is_compaction_baseline is dead (implementation review 2026-08-09, finding L-7). The writer inserts a literal 0 into it and no read path anywhere consults it — compaction is handled entirely by the repricer over parsed boundaries (§6), not by a flag on a row. It survives because dropping a column costs a full table rebuild for no behavioral gain; what it must not do is mislead a reader into thinking a baseline flag is what makes compaction repricing work.

The source docs name these three dimensions as rate-changing without enumerating their literal values or exact column types — that detail is schema work not yet written (owned by WP-D8 in the development plan, Track D, wave 10). Two further properties are load-bearing and are specified:

The authoritative DDL is migration 6, quoted in the as-built note above — the illustrative wide-format sketch this page originally carried (per-dimension columns, compaction_baseline_id, an FK to agents) never became the real table.

The ingest work that populates this table shipped as the parser writing projections directly (see ingest & reconciliation): tokens copied verbatim — no inference — and “Σ token_usage per session == JSONL exact” is one of the three P0 release-blocker tests, built and green in the server suite.

3. model_pricing: a dated table, not a hardcoded constant

The pricing side of the equation is the second hoangsonww graft (DESIGN §4): model_pricing “to drive the dollar-cost + delegation-savings tile.” The design decision that governs it (D6 in the implementation plan) is explicit about why it must be dated rather than a single constant:

Decision. A single model_pricing seed (dated, with model IDs) is the source of truth; a test asserts every model seen in fixtures has a price, and the constant carries a “verified on <date>” stamp. Re-verify rates on each cost-feature change. Why. Turns a silent staleness liability into a failing test.

This directly answers the anti-pattern this system deliberately avoids. cast’s delegation-savings formula is worth stealing (§7 below), but its pricing table is, in the source, hardcoded and stale (concept-analysis.md §6.4). The concept review names the concrete threat: the model lineup is actively churning (named examples as of the source analysis: Opus 4.8, Sonnet 5, Haiku 4.5, Fable 5 — concept-analysis-v2 §5, Weaknesses) and “there is no stated source-of-truth for prices, no update process, and no test that fails when a rate goes stale. A wrong dollar figure quietly discredits the whole cost moat” (concept-analysis.md §6.4).

CD-4’s canonical schema rule is the fix:

versioned model_pricing (effective_from, verified_on).

The real table is migration 7 (model-pricing-with-seed):

CREATE TABLE model_pricing (
  model          TEXT NOT NULL,
  bucket         TEXT NOT NULL CHECK (bucket IN ('input','output','cache_read','cache_write_5m','cache_write_1h')),
  usd_per_mtok   REAL NOT NULL,
  effective_from TEXT NOT NULL,
  PRIMARY KEY (model, bucket, effective_from)
);

Multiple effective_from rows can exist for the same (model, bucket) without conflict — that is the whole point of dating the table, and the composite primary key enforces it. Two honest departures from the CD-4 sketch:

What is genuinely undecided: the authoritative dated source for these rates and the refresh cadence that keeps the staleness-fails-CI test honest as the model lineup churns is an open Phase-0 input, not yet answered (concept-analysis-v2 §7, question 7: “the authoritative dated source for model_pricing and the refresh cadence that keeps the staleness-fails-CI test honest”). This page does not assert real dollar figures for that reason — the rate columns above are placeholders, not published prices.

4. PricingProvider: a dated lookup, not “the current price”

Pricing a historical event correctly requires resolving the rate that was in effect when the tokens were spent, not the rate in effect right now. This is the PricingProvider port named in the canonical adapter set (CD-6; see the ports & adapters table on the overview page). Its contract, from the development plan (WP-C2):

PricingProvider port + timestamp-aware dated-price resolver. An event resolves to the OLD rate before a change, the new rate at/after.

Conceptually, the resolver is a “latest row at or before this timestamp” lookup per bucket:

SELECT price_per_input_token, price_per_output_token
FROM model_pricing
WHERE model_id = :model_id
  AND speed = :speed AND inference_geo = :inference_geo AND service_tier = :service_tier
  AND effective_from <= :event_timestamp
ORDER BY effective_from DESC
LIMIT 1;

This is why a rate change (a new model_pricing row with a later effective_from) never rewrites the cost of events that already happened — the dated resolver picks the row that was live at the time, per bucket.

As built: exactly this lookup shipped, twice. resolveRate in packages/core/src/cost/compute-cost.ts picks the latest effective_from at or before the usage row’s occurred_at for the exact (model, bucket); the read-side SQL in apps/server/src/api/queries.ts performs the same dated join. A bucket with zero tokens needs no price row; a bucket with nonzero tokens and no resolvable rate raises PricingError in the engine — and in the read aggregates is surfaced as unpricedTokens rather than silently priced.

AMENDED 2026-09-23 (J-2). “No resolvable rate” used to mean only “no row”. It now also means “a row whose stored rate cannot be used”, and the two halves of the system answer that case differently. Keep them apart - blurring them is how a reader concludes that a bad rate halts ingest, which it does not.

Case Where What happens
Model id / bucket with nonzero tokens and no dated row at or before occurred_at computeCostUsd, packages/core/src/cost/compute-cost.ts PricingError, unchanged. At ingest this halts before the write transaction opens; at GET /api/sessions/:id/cost-analysis it is a 422. The docblock still reads “silence here is phantom-free but money-wrong”.
A dated row exists but usd_per_mtok is negative, non-finite or text-coerced the read SQL in apps/server/src/api/queries.ts Resolves to NULL: the tokens are served as unpriced and contribute $0, visibly. No halt, no 500, and no dollar figure derived from the bad value.

Three specifics worth stating, because each was a live wrong answer before:

5. CostEngine: every dollar = tokens × dated price

CostEngine (CD-6) is the pure computation that combines token_usage with PricingProvider lookups (development plan WP-C3, Track C, Phase 3):

CostEngine — ground-truth tokens × dated bucketed price. Cost matches a hand-computed value from JSONL tokens × seed.

In formula form, for a session or agent:

cost = Σ over each token_usage row r:
         PricingProvider.rateFor(r.model_id, r.speed, r.inference_geo,
                                  r.service_tier, r.recorded_at)
         × r.tokens

Nothing in this equation infers, estimates, or rounds a token count — the only computed quantity is the price lookup, and that lookup is itself a deterministic, dated function of data already in model_pricing. The Phase-4 read-API exit gate restates the same guarantee from the consumer side: “every displayed dollar traces to ground-truth tokens × dated price” (development plan §3, Phase 4 exit gate), and WP-U4’s Done-when is explicit that this must hold with no API-side inference.

As built: computeCostUsd is a pure function (no DB imports) in packages/core/src/cost/compute-cost.ts, and it doubles as the ingest halt gate: apps/server/src/ingest/ingest-session.ts prices the parsed session before opening the write transaction, so a PricingError means no row of any kind is written — a session is never persisted with silently unpriceable tokens. The same error surfaces as a 422 from GET /api/sessions/:id/cost-analysis.

6. Compaction-baseline repricing

PreCompact is one of the twelve (nine confirmed, per the hook-catalog caveat — see hook ingestion; as built it is one of the four hooks the installer actually registers) Claude Code lifecycle events this system ingests (DESIGN §5). When a session’s context is compacted, a naive costing scheme that only tracks a running total risks silently corrupting history — this is exactly the “compaction sleeper” the source analysis flags: getting the compaction-baseline graft wrong “means silently wrong dollar figures” (concept-analysis.md §3.3), which is why “compaction-mid-session and PreCompact re-pricing” was added as a dedicated regression case on top of the ten EXPANDED §7.1 negative scenarios (concept-analysis-v2 §6, Tree correctness).

The fix is structural, not a patch: token_usage preserves the pre-compaction baseline rather than discarding it (DESIGN §4), and the CostEngine reprices against that preserved baseline plus the post-compaction spend:

Compaction-baseline preservation + RE-pricing across PreCompact. A PreCompact session reprices to baseline + post-compaction spend, matching the oracle. (development plan WP-C4)

The MVP acceptance criterion states the outcome directly: “a session that hit PreCompact still reprices correctly against its preserved baseline” (concept-analysis-v2 §6, Cost). This is also called out as one of the two v1-only test cases added on top of the negative-test catalogue (compaction-mid-session and PreCompact re-pricing — concept-analysis-v2 §4.3).

What is genuinely undecided: whether the JSONL transcript itself carries pre/post-compaction markers precise enough to reconstruct the baseline after the fact, or whether the baseline must be snapshotted at hook time instead, is an open Phase-0 question (G0.2b, concept-analysis-v2 §7, question 4). Until that probe runs, treat “compaction reprices correctly” as the target contract, not a confirmed implementation mechanism.

Resolved as built: the JSONL branch won. Compaction boundaries are detected in the parsed transcript substrate itself — no hook-time snapshot exists, and the PreCompact hook contributes a liveness timestamp only. Note that no row is flagged as a baseline: token_usage.is_compaction_baseline is a dead column (§2), and the whole mechanism lives in the repricer (packages/core/src/cost/compaction-repricing.ts), which cuts each transcript’s usage stream at its own boundaries and prices the segments.

Three segmentation rules are settled, and one of them was open when this page was first written:

The result deliberately exposes both figures — naiveUsd (the boundary-blind single-pass sum, which is exactly computeCostUsd) and repricedUsd (the sum of per-segment costs) — so that their difference is inspectable rather than assumed. That difference is the exit gate. On a complete, correctly deduped substrate the per-message usage rows are ground truth and cost is linear over them, so deltaUsd must be ~0 within floating-point epsilon. A materially nonzero delta means rows were lost or double-assigned during segmentation; it is a loud mispricing signal, never something to average away. What the segmented view buys over the naive sum is not a different total but the breakdown: where each context reset falls, the preTokens baseline it preserved, and how much spend belongs to each cache lineage.

7. Delegation-savings: quantifying Haiku/Sonnet routing

The moat backlog names this explicitly as a build target, not a nice-to-have (DESIGN §2, item 2):

Live dollar-cost attribution + delegation-savings (Haiku/Sonnet routing vs top-tier). Borrow cast’s ~50-LOC formula.

The pattern being borrowed — the idea, re-priced clean-room, never the source code (see §8 below) — is scoped explicitly in the implementation plan’s Phase-3 delegation-savings tile, which names cast’s analytics.ts as the (never-copied) source of the idea:

Delegation-savings metric — reimplemented clean-room from cast’s idea (all-rights- reserved; do not copy analytics.ts): conservative max(0, sonnetEquiv − actualHaiku) off ground-truth JSONL tokens.

— implementation-plan.md, Phase 3 — Delegation-savings tile

agenthropic’s version generalizes the two-model comparison into a top-tier re-price across whatever model actually ran (development plan WP-C5, Track C):

Delegation-savings metric (clean-room, tied to the model-routing decision). Savings = Σ max(0, top-tier-equiv − actual), matching a hand check.

In formula form, per session or agent:

delegation_savings = Σ over each token_usage row r where r.model_id != top_tier_model:
                        max(0,
                            PricingProvider.rateFor(top_tier_model, r.bucket…, r.recorded_at) × r.tokens
                            − PricingProvider.rateFor(r.model_id,    r.bucket…, r.recorded_at) × r.tokens
                        )

This is the direct dual-price extension the roadmap names for the delegation-savings tile — “Dual-price token_usage (actual vs top-tier) → quantify Haiku/Sonnet routing savings” (DESIGN §9). Note that the development plan’s reconciled Phase sequencing lands the whole cost engine — including delegation-savings (WP-C5) — in Phase 3 (Track C, development-plan §3), which supersedes DESIGN §9’s earlier draft placement of “delegation-savings tile” as a standalone Phase 4 item; treat development-plan.md as the canonical phase numbering per its own provenance note.

As built: shipped in packages/core/src/cost/delegation-savings.ts with the conservative formula intact — savings = Σ max(0, hypothetical − actual) per agent — and served through GET /api/sessions/:id/cost-analysis. The honesty labels are structural, not cosmetic: the result carries a literal isEstimate: true type (it can never be reported as a measured number), agents whose model cannot be established are listed in skippedAgentIds rather than priced against a guess, and '<synthetic>' rows keep their own (zero-rate) model instead of being re-priced at top tier.

Which model counts as “top tier” is answered per subagent, not globally. The plan left this free; the implementation resolves it in three steps. An explicit topTierModel wins — the caller names the routing alternative being measured, and the API exposes it as an optional query parameter on GET /api/sessions/:id/cost-analysis. Otherwise the model is derived from the subagent’s nearest ancestor that actually has usage: walk parentAgentId upward (main-agent usage carries a null agent id) behind a defensive cycle guard, and take that ancestor’s settled model — the model of its greatest-output usage row, following the message-id dedup convention. And if the ancestor chain yields no model at all — an orphan, a dangling parent pointer, a cycle — that subagent has no observable routing decision, so it is excluded from the sums and named in skippedAgentIds. That is the same refusal the rest of the system makes: the number omitted is more useful than a number invented.

The estimate label is earned rather than decorative. The hypothetical carries the subagent’s actual cache-read/cache-write profile over unchanged, but had the work truly run inline the parent’s cache lineage would have differed in both directions — more cache reads over a bigger prefix, no separate context warm-up, possibly earlier compaction — and no ground truth for that counterfactual exists anywhere in the substrate.

Known arithmetic caveat (implementation review 2026-08-09, finding L-8). The max(0, …) clamp is applied per agent before summing, so the reported total is the sum of non-negative per-agent terms, not max(0, Σ hypothetical − Σ actual). The two differ whenever some delegation cost more than its top-tier equivalent would have: those agents contribute 0 instead of a negative offset, which makes the headline figure optimism-leaning by construction. This matches the plan’s “conservative max(0, sonnetEquiv − actualHaiku)” wording read per-term, and it is recorded here rather than silently reconciled — the per-agent breakdown is served alongside the total precisely so a reader can do the other arithmetic.

A named risk, and its mitigation. concept-analysis-v2 flags this metric directly as a vanity-metric risk: “delegation-savings risks being a vanity metric unless the decision it informs is named” (§5, Weaknesses). The mitigation on record is to tie the figure to a concrete routing decision rather than display it as a standalone number — “tie delegation-savings to the named routing decision (WP-C5) so it isn’t vanity” (best-path-decision.md §6, item 6) — i.e., the metric must inform whether to route a given workload to a cheaper model, not merely decorate a dashboard tile.

8. Licensing: clean-room, not copy-paste

cast’s delegation-savings formula is a pattern worth stealing; its source code is not. cast carries no license field and no LICENSE file — “MIT” appears only as a README badge, which under Berne convention makes it all-rights-reserved by default (concept-analysis.md §6.2; concept-analysis-v2 CD-9). The canonical rule:

CLEAN-ROOM reimplement cast controlGate + delegation-savings and nirdiamant checkpoint (never view their source while writing). Enforced by a CI provenance/license scan. (concept-analysis-v2 CD-9)

This is enforced structurally, not by policy alone — a CI license/provenance scan is a build-failing gate (development plan WP-F6; Phase 1 exit gate: “no-spawner/no-SSRF/ license gates green”). Full rule and enforcement detail: licensing & provenance.

9. The no-priceless-model-fails-CI rule

This is the rule the task scope names directly, and it is the load-bearing test that makes every other guarantee on this page trustworthy rather than aspirational. Two equivalent statements of it appear across the source material:

The design intent is explicit that this must be a build failure, not a runtime degradation: “silent staleness is a red build, not a runtime ‘estimated’ label” (concept-analysis-v2 §3, H-COST). Concretely, WP-C6 depends on WP-C1/WP-C2 (the pricing table + resolver) and WP-X1/WP-X5 (the golden fixture corpus and the CI coverage gate) — every model+bucket combination that appears in the golden fixture corpus must resolve to a priced row, or the merge is blocked (Phase 3 exit gate, development-plan §3: “no priceless model” is one of the named, merge-blocking release criteria alongside the three P0 reconciliation tests).

As built — an honest status split. The runtime half of this rule is built and test-proven: a priceless model with nonzero tokens raises PricingError, which halts ingest before any write and returns 422 from the cost API — strictly stronger than a runtime “estimated” label. The CI half — a dedicated staleness gate that turns a priceless fixture model into a red build (WP-C6) — has not been verified as wired into CI at the time of this update; until that is confirmed, treat “staleness fails CI” as design intent backed by the runtime halt, not as an observed CI behavior.

One clarification the paragraph above needs since 2026-08-25: “or the merge is blocked” is no longer the uncertain half of that sentence. main is branch-protected and requires the ci check, so a red run does withhold the merge button — from a contributor. It does not withhold it from the repository owner, whose exemption is deliberate and stated rather than accidental: enforce_admins is off because agenthropic has one maintainer whose normal working mode is a direct push to main (the standing correction). None of that moves this section’s verdict. No WP-C6 staleness gate has been verified to run as a step inside the ci job, and a gate that never runs blocks nobody’s merge, however main is protected. (A gate added as a separate job would not block one either: ci is the only required context.)

10. Read surface (as built: shipped)

The cost figures this page describes are computed server-side; nothing here is a client-side estimate. The read surface is now real: GET /api/cost/summary (per-model and per-session dollar totals, with unpricedTokens surfaced) and GET /api/sessions/:id/cost-analysis (compaction-aware repricing plus delegation savings, isEstimate visible), both in apps/server/src/api/routes.ts, plus the SPA cost view built over them. The work packages as planned:

Work package Track What it exposes
WP-C7 cost (backend) Cost query API — TypeBox-validated, loopback+token-gated cost/savings endpoints; “figures match direct engine calls (no drift).”
WP-U4 backend Cost, delegation & global-DAG read endpoints (daily question 2/4); “cost matches JSONL × versioned pricing; no API-side inference.”
WP-U9 frontend Cost / Sankey / delegation-savings view; “every displayed dollar traces to ground-truth tokens × dated price.”

These endpoints inherit the same security posture as every other write/read surface in the system — loopback bind, mandatory timingSafeEqual token, same-origin SSE (see security model) — there is no cost-specific exemption.

11. Why this needs no ANTHROPIC_API_KEY

The cost model is a pure computation over data already resident in SQLite — ground-truth token counts from token_usage and dated rates from model_pricing. It makes no live API calls to price anything, so it does not need the Anthropic API key in the dashboard’s environment at all. This aligns with the standing security rule: “don’t hold ANTHROPIC_API_KEY in the dashboard’s env unless a feature truly requires it” (DESIGN §8), reaffirmed by CD-10 (“ANTHROPIC_API_KEY stays out of the dashboard env entirely” — the only feature that would need it is the labeled-experimental vector-DB track, which is off the critical path per the roadmap).

What’s undecided

This was a design-basis page; the list below records what was open when it was written, with the as-built resolution appended to each item:

See also