# Configuration reference

Configure gateway environment values in `docker/.env`. Learner settings use the separate `docker/learner.env` file and appear at the end of this page.

<!-- GENERATED FROM docker/.env.example - DO NOT EDIT BY HAND.
     Regenerate: ./mvnw test -Dtest=ConfigReferenceTest -Dpointsman.docs.write=true
     Guarded by ConfigReferenceTest. -->

## Reading this reference

Each setting lists its declared value and the conditions under which it is required.

| Declaration | What to do |
|---|---|
| Defaults to a value | Omit it to use that default; add an environment entry to override it |
| No default; you supply this | Supply the value according to the requirement described with the setting |
| Required once a feature is selected | Supply it when enabling that feature; it is ignored otherwise |
| Shipped as a value | The example environment file explicitly sets that value |

**Coverage:** this generated catalog covers `docker/.env.example` and the learner environment example. It is not a complete inventory of every Spring property. Settings bound through configuration types or read directly from the environment may be documented in feature guides instead.

For scopes, activation and dashboard editability, use [configuration ownership](dashboard-configuration.md). For a setup workflow, use [configure Pointsman](../how-to/configure-pointsman.md).

## REQUIRED. Compose refuses to start without these two.

The key-hash pepper. The gateway refuses to start without one. STABLE ACROSS RESTARTS: a changed pepper voids every key ever issued (the stored fingerprint turns that into a named startup refusal rather than a silent mass 401). Treat like the database password.

- `POINTSMAN_AUTH_PEPPER` - no default; you supply this.

Declared, never defaulted. 'live' for production, 'test' otherwise. Keys carry this segment (pointsman_live_... / pointsman_test_...) and a gateway refuses keys minted for the other environment, by name.

- `POINTSMAN_KEY_ENVIRONMENT` - no default; you supply this.

## PROVIDER CREDENTIALS

Absent -> the gateway refuses to start naming the missing key (Spring AI builds its clients at boot). For a control-plane-only trial with no /v1 traffic reaching providers, placeholders suffice - and the gateway ANNOUNCES a placeholder-shaped key at startup, because requests routed to that provider will fail at call time. A placeholder is a trial accommodation, never a production setting. `OPENAI_API_KEY=dummy-key` `ANTHROPIC_API_KEY=dummy-key`

- `OPENAI_API_KEY` - no default; you supply this.
- `ANTHROPIC_API_KEY` - no default; you supply this.

Row-vendor keys, referenced BY NAME from registry provider rows (credential_ref "env:DEEPSEEK_API_KEY"; see scripts/onboard-providers.sh). Unlike the two above these are not required at boot: absent only fails requests routed to that vendor's row. Set the ones whose rows you onboard.

- `DEEPSEEK_API_KEY` - no default; you supply this.
- `XAI_API_KEY` - no default; you supply this.
- `MOONSHOT_API_KEY` - no default; you supply this.
- `GROQ_API_KEY` - no default; you supply this.
- `GEMINI_API_KEY` - no default; you supply this.

Provider endpoints. Override for a proxy, a gateway of your own, or a self-hosted compatible server.

- `OPENAI_BASE_URL` - defaults to `https://api.openai.com`.
- `OLLAMA_BASE_URL` - defaults to `http://host.docker.internal:11434`.

First-boot admin. Seeds ONLY into an empty admin table; blank seeds no admin and warns at startup.

- `POINTSMAN_ADMIN_USERNAME` - no default; you supply this.
- `POINTSMAN_ADMIN_PASSWORD` - no default; you supply this.

Gateway cgroup memory limit (not Java heap). The evaluation default is 2g; select a limit from the qualified workload profile, not from heap size alone. A larger value changes JVM heap ergonomics; it is not a demonstrated capacity gain.

- `POINTSMAN_GATEWAY_MEMORY_LIMIT` - defaults to `2g`.

These endpoint settings are honored by Compose when this file is passed with --env-file docker/.env. Defaults address the bundled services. External endpoints do not remove the bundled services or their dependency checks. POSTGRES_DB/USER also initialize the bundled database on FIRST BOOT; changing them does not migrate an existing data volume or create a new role in it.

- `POSTGRES_HOST` - defaults to `postgres`.
- `POSTGRES_PORT` - defaults to `5432`.
- `POSTGRES_DB` - defaults to `pointsman`.
- `POSTGRES_USER` - defaults to `pointsman`.
- `POSTGRES_PASSWORD` - defaults to `pointsman`.

Maximum time the request path waits to obtain a PostgreSQL connection. On a cold key-cache miss, a database outage becomes a retryable 503 after this budget rather than Hikari's hidden 30-second default. Keep it below the client timeout and above ordinary connection jitter.

- `POINTSMAN_DATABASE_CONNECTION_TIMEOUT_MS` - defaults to `2000`.

Bounds on a database call once a connection is held. The statement timeout (milliseconds) cancels a slow or lock-blocked statement on the server. The socket timeout (SECONDS, pgjdbc's unit) ends a wait on a server that stops answering entirely; keep it above the statement timeout. The connect timeout (SECONDS) bounds opening a connection. They apply to every statement on the gateway's pool, background jobs included; Flyway migrations use their own connection and are not bounded. Raise them if a legitimate job needs longer.

- `POINTSMAN_DATABASE_STATEMENT_TIMEOUT_MS` - defaults to `10000`.
- `POINTSMAN_DATABASE_SOCKET_TIMEOUT_SECONDS` - defaults to `15`.
- `POINTSMAN_DATABASE_CONNECT_TIMEOUT_SECONDS` - defaults to `5`.

Two Redis connections by design - the cache and the rate-limit counters, splittable to two hosts for memory isolation. Both point at one instance in this stack.

- `REDIS_CACHE_HOST` - defaults to `redis-stack`.
- `REDIS_CACHE_PORT` - defaults to `6379`.
- `REDIS_CACHE_PASSWORD` - defaults to empty.
- `REDIS_COUNTER_HOST` - defaults to `redis-stack`.
- `REDIS_COUNTER_PORT` - defaults to `6379`.
- `REDIS_COUNTER_PASSWORD` - defaults to empty.

How long any one Redis command may take before it is abandoned. Lettuce's own default is 60 SECONDS, which meant a Redis that accepted connections without answering held a request for a minute; both of these endpoints are read on the request path, so both carry a short budget instead. Raising it widens the window in which a hanging Redis holds connections. Redis topology. Declared, never guessed: a mode inferred from the host list would give standalone behaviour against a cluster, which works until a key moves. Seeds are enough - Lettuce discovers the rest from any reachable member. NOTE the compose stack runs redis/redis-stack, a single-node distribution that cannot run cluster mode; cluster means your own Redis 8.

- `REDIS_COUNTER_MODE` - defaults to `standalone`.
- `REDIS_COUNTER_NODES` - defaults to `redis-1:6379,redis-2:6379,redis-3:6379`.
- `REDIS_CACHE_MODE` - defaults to `standalone`.
- `REDIS_CACHE_NODES` - defaults to empty.

The counter store's budget is 1000 ms since v1 qualification (26 Sep 2026): a 280 ms counter stall at 250 ms opened the breaker and refused ten seconds of capped traffic.

- `REDIS_COUNTER_COMMAND_TIMEOUT_MS` - defaults to `1000`.
- `REDIS_CACHE_COMMAND_TIMEOUT_MS` - defaults to `250`.

Both endpoints are guarded by a circuit breaker as well: five consecutive health failures (connection refused, pool exhaustion, command timeout) stop the calls for one second (POINTSMAN_REDIS_BREAKER_OPEN_MS), then one probe. Its state is on /actuator/health under counterStore. The policy service (the bandit's learner). POLICY_SERVICE_URL is the shared address; POINTSMAN_BANDIT_POLICY_SERVICE_URL narrows it for the bandit alone, and falls back to the shared one when unset.

- `POLICY_SERVICE_URL` - defaults to `http://policy-service:8081`.
- `POINTSMAN_BANDIT_POLICY_SERVICE_URL` - defaults to empty.

Where the dashboard's charts query metrics from.

- `POINTSMAN_OBSERVABILITY_PROMETHEUS_URL` - defaults to `http://localhost:9090`.

The container's memory as the kernel accounts it (cgroup v2 working set and limit, read from the gateway's own cgroup files; docs/operating-notes.md#gateway-memory-alerts). ON by default; reads at most once per interval. A different cgroup mount needs PATH; unreadable files export NaN and fire GatewayMemoryUnobservable, never zero.

- `POINTSMAN_OBSERVABILITY_CGROUP_MEMORY_ENABLED` - defaults to `true`.
- `POINTSMAN_OBSERVABILITY_CGROUP_MEMORY_PATH` - defaults to `/sys/fs/cgroup`.
- `POINTSMAN_OBSERVABILITY_CGROUP_MEMORY_MIN_INTERVAL_MS` - defaults to `5000`.

Where an admin-set destination (registry provider base_url, guardrail URLs) may point; docs/security.md#outbound-destinations. Link-local/metadata are refused; loopback/private only when listed (host names, IPs or CIDRs). Not egress isolation.

- `POINTSMAN_EGRESS_ENFORCE` - defaults to `true`.
- `POINTSMAN_EGRESS_ALLOWED_PRIVATE` - defaults to empty.
- `POINTSMAN_EGRESS_ALLOW_LINK_LOCAL` - defaults to `false`.

Tracing exports default OFF. Without the collector (the opt-in tracing profile) the exporter dials an absent localhost:4318 and fills the logs with export errors; a fresh install's first `docker logs` should not read as broken. Set true alongside --profile tracing.

- `POINTSMAN_OTEL_ENABLED` - defaults to `false`.
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - defaults to `http://localhost:4318/v1/traces`.
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` - defaults to `http://localhost:4318/v1/logs`.

Host port remaps for compose itself. The gateway publishes a RANGE so `--scale gateway=2` just works. The default stack publishes only the gateway and the dashboard; the stores and the observability pair are compose-network only. So the four store and observability variables below remap nothing on their own - they take effect under the dev overlay (-f docker/docker-compose.yml -f docker/docker-compose.dev.yml) or in docker-compose.minimal.yml, both of which bind to 127.0.0.1. Override them when something on your host already holds the port.

- `POINTSMAN_PG_HOST_PORT` - defaults to `5432`.
- `POINTSMAN_REDIS_HOST_PORT` - defaults to `6379`.
- `POINTSMAN_REDISINSIGHT_HOST_PORT` - defaults to `8001`.
- `POINTSMAN_PROMETHEUS_HOST_PORT` - defaults to `9090`.
- `POINTSMAN_GRAFANA_HOST_PORT` - defaults to `3000`.
- `POINTSMAN_POLICY_HOST_PORT` - defaults to `8081`.
- `POINTSMAN_GATEWAY_HOST_PORTS` - defaults to `8080`.

What the gateway requires of the context arithmetic before admitting a request (September 2026 review). provider-compatible (the default) admits what the selected model's provider would accept, which includes an over-ask on a model whose row declares that its provider truncates at the window instead of refusing. reserve-output requires room for the whole requested output even there. Neither ever shrinks a cap the caller sent. See docs/gateway-controls.md.

- `POINTSMAN_CONTEXT_ADMISSION_POLICY` - defaults to `provider-compatible`.

What the budget does when the counter Redis is down and a spend cap applies. deny (the default) refuses with a 503 (budget_unverifiable) without reading the usage ledger, including a caller already over a cap the ledger could show; allow admits on a ledger estimate. Traffic with no enforcing spend cap is unaffected either way. See docs/gateway-controls.md.

- `POINTSMAN_BUDGET_FAILURE_MODE` - defaults to `deny`.

Under allow, at most this many ledger reads run at once while the counter Redis is down; a request that finds none free is refused with the same 503, rather than queueing for a database connection. deny never reads the ledger during an outage.

- `POINTSMAN_BUDGET_LEDGER_FALLBACK_MAX_CONCURRENT` - defaults to `4`.

What the budget fence does when its store checks fail (budget recovery design 4.1). The fence detects a restarted, failed-over or reloaded Redis by run_id on its own; it detects an in-process restore or eviction only when the gateway's Redis user is denied RESTORE, SWAPDB, FLUSHALL, FLUSHDB, DEBUG and CONFIG SET, and the server runs maxmemory-policy noeviction. report (the default) shows a failing check on health and pointsman.budget.store_checks_failing and keeps budgets open; enforce refuses budgeted requests with 503 budget_recovering while a check fails. The shipped Compose runs one shared Redis with no ACL user, so the checks fail there. See docs/known-issues.md, KI-7.

- `POINTSMAN_BUDGET_FENCE_STORE_CHECKS` - defaults to `report`.

How often the budget fence store checks run, in milliseconds. Health also reports how old the last result is (lastCheckAgeMs) and flags it stale after three intervals, with the gauge pointsman.budget.store_checks_stale. A stale PASS keeps admitting: checks that stop running are an operator signal, not a reason to refuse every budgeted request.

- `POINTSMAN_BUDGET_FENCE_STORE_CHECK_MS` - defaults to `60000`.

How long a recovery recheck keeps trying for the single-inspection permit before giving up. The counter breaker closing asks for a fresh store check rather than waiting for the schedule above; that recheck shares one permit with the scheduled sweep, so it may have to wait for a sweep already in progress. Too short and the recheck is abandoned while the sweep that beat it publishes a result taken BEFORE the store recovered, leaving budgets fenced until the next schedule - the failure this wait exists to avoid. Falls back to the periodic check either way.

- `POINTSMAN_BUDGET_FENCE_RECHECK_WAIT_MS` - defaults to `30000`.

THE RESTORE DECLARATION (restore procedure step, docs/operating-notes.md "Restore"). Set it on EVERY gateway instance started after a PostgreSQL restore, to an id naming the restore (for example the backup label). At startup, before traffic, it records a recovery boundary; a tenant first seen without a budget trust row whose scope predates the boundary then starts RECOVERING (503 budget_recovering, reopened by attestation) instead of trusted at zero - the restored database cannot show spend made after the backup. The same id at a later restart is a no-op; if the boundary cannot be recorded the gateway refuses to start. Empty (the default) declares nothing. A counter-store loss is also detected on its own when a previously active tag's epoch is missing, but that cannot see a deployment with no active epochs: the declaration is the supported step.

- `POINTSMAN_BUDGET_RECOVERY_RESTORE_ID` - defaults to empty.

A COOPERATIVE elapsed-time budget for ONE advisory balance read. It is NOT the per-query timeout on the spend sums, and it is NOT a hard deadline - the distinction matters, so read it before relying on either. What it does: the clock starts before the rule lookup, and the read checks it before each spend sum and again before returning. Once the budget is gone it starts no further work and returns NO balance - not even one it has already computed, because that balance was bought with time the budget did not cover. What it does NOT do: interrupt anything already in flight. It cannot cut short a rule lookup, a connection acquisition, or a query that has already begun. With a 50 ms budget, a 10 ms lookup followed by a 900 ms sum still occupies the request for about 910 ms; the difference this setting makes is that the request then hands back nothing instead of a stale figure. Bounding the elapsed time itself would mean propagating the remaining budget into connection acquisition and statement execution and cancelling in a way that releases the connection; that is not implemented. What bounds each individual query is the fixed one-second CEILING on the spend sums (a JPA query hint cannot take a property placeholder, so it is not settable here). What bounds the fleet is POINTSMAN_BUDGET_LEDGER_FALLBACK_MAX_CONCURRENT. An identity can be subject to four caps - credential, principal, app scope, org scope - each a separate query, so that ceiling alone permits a multi-second operation. The balance is best-effort (a 402 body, the affordability filter) and is never the gate, so giving up costs nothing but the figure, and is counted on pointsman.budget.oracle.advisory_budget_exceeded. Must be positive; the gateway refuses to start otherwise.

- `POINTSMAN_BUDGET_ADVISORY_READ_BUDGET_MS` - defaults to `2000`.

Sample credentials, and the two that are knobs. Set these before the stack sits anywhere with an address other people can reach (docs/security.md, "What the stack publishes").

- `POSTGRES_PASSWORD` - defaults to `pointsman`.
- `GF_SECURITY_ADMIN_PASSWORD` - defaults to `admin`.

Ad-hoc boot config as a JSON blob (the learning demo injects its routing group this way). Boot-scoped: it leaves on the next re-up.

- `SPRING_APPLICATION_JSON` - defaults to empty.

## BEHAVIOUR. Everything below has a working default; change what you need.

## ROUTING AND FALLBACK

The strategy a model group uses when it names none. See docs/routing-strategies.md for the menu and what each does badly.

- `POINTSMAN_ROUTING_DEFAULT_STRATEGY` - defaults to `weighted-random`.

How long a rate-limited KEY sits out when the provider did not say, and the jitter that stops every instance re-probing it in the same second. When the 429 carries a wait (Retry-After in seconds or as a date, retry-after-ms, or the earliest OpenAI x-ratelimit-reset-* / Anthropic *-reset header), the gateway waits THAT long instead of the default - up to the ceiling below, past which it clamps and says so in the cooldown log line. A provider asking for six hours is not getting six hours: one probe every five minutes is the cost of learning it has recovered sooner. Neither number reaches the circuit breaker, which a rate limit never opens (it is the key's condition, not the provider's health; 5xx, timeouts and connection faults are the breaker's).

- `POINTSMAN_ROUTING_COOLDOWN_SECONDS` - defaults to `5`.
- `POINTSMAN_ROUTING_COOLDOWN_JITTER_MS` - defaults to `2000`.
- `POINTSMAN_ROUTING_COOLDOWN_MAX_SECONDS` - defaults to `300`.

The fraction of a declared provider limit (POINTSMAN_ROUTING_PROVIDER_LIMITS_* in routing.yml, or a credential pool member's declared tpm) the router will fill before it treats that provider or member as at capacity and looks elsewhere. Headroom is limit x threshold minus the rolling one-minute tally. Raise toward 1.0 to run providers hotter (more upstream 429s); lower it to leave more slack. Was a compiled-in 0.90 the control surface could only describe until 2026-09-02. Reported live at GET /admin/control/effective.

- `POINTSMAN_ROUTING_CAPACITY_THRESHOLD` - defaults to `0.90`.

The latency EWMA behind latency-based routing and the fallback ladder's health view: a new sample contributes alpha, history 1 - alpha. Closer to 1.0 reacts faster and forgets faster; closer to 0 smooths harder. In (0, 1]. Compiled in at 0.3 until 2026-09-02.

- `POINTSMAN_ROUTING_LATENCY_EWMA_ALPHA` - defaults to `0.3`.

The fallback ladder: how many deployments one request may try, and whether it may leave the requested quality tier in each direction.

- `POINTSMAN_FALLBACK_MAX_ATTEMPTS` - defaults to `5`.
- `POINTSMAN_FALLBACK_ALLOW_TIER_UP` - defaults to `true`.
- `POINTSMAN_FALLBACK_ALLOW_TIER_DOWN` - defaults to `true`.

What an unresolvable model name becomes: fallback-to-allowed (collapse into the blank-model flow), resolve-only (400 model_unresolvable), or reject (only exact ids accepted). And what an evidently-intended but not-permitted model does: reject (403) or substitute. The shipped Compose profiles set resolve-only (fail closed); the bare-jar application default is still fallback-to-allowed.

- `POINTSMAN_RESOLUTION_UNKNOWN_MODEL` - defaults to `resolve-only`.
- `POINTSMAN_RESOLUTION_ON_DENIED` - defaults to `reject`.

What a request that names NO model gets: route (fail safe, the default: key default model, semantic group, smart-routing tier, then the cheapest allowed model - disclosed in headers) or refuse (fail fast: 400 model_required, nothing served or billed). Unknown values refuse the boot.

- `POINTSMAN_RESOLUTION_OMITTED_MODEL` - defaults to `route`.

Route filters run before selection. Name filters here (comma-separated) to skip them; every RouteFilter bean is otherwise auto-discovered.

- `POINTSMAN_ROUTING_DISABLED_FILTERS` - defaults to empty.

Semantic routing: the cosine floor a prompt must clear against a group's description embedding before that group is chosen.

- `POINTSMAN_ROUTING_SEMANTIC_THRESHOLD` - defaults to `0.75`.

Smart routing's two lines, for blank-model requests when smart routing is on. Each line is a token coordinate OR a message coordinate: at or above either, the request routes to that tier. Quality outranks balanced, images always route quality, and a system prompt alone reaches balanced. Below both lines is the cost tier. Tokens here are the chars-per-token estimate (POINTSMAN_ESTIMATE_CHARS_PER_TOKEN), not provider counts. The balanced line must not sit above the quality line; the boot refuses that. These were compiled-in constants with a rationale but no measurement behind them until 2026-09-02; the defaults are unchanged.

- `POINTSMAN_ROUTING_SMART_QUALITY_TOKEN_THRESHOLD` - defaults to `2000`.
- `POINTSMAN_ROUTING_SMART_QUALITY_MESSAGE_THRESHOLD` - defaults to `6`.
- `POINTSMAN_ROUTING_SMART_BALANCED_TOKEN_THRESHOLD` - defaults to `500`.
- `POINTSMAN_ROUTING_SMART_BALANCED_MESSAGE_THRESHOLD` - defaults to `3`.

latency-based routing spreads traffic across everything within this multiple of the fastest arm. Raise to spread more, lower to be stricter.

- `POINTSMAN_ROUTING_LATENCY_BUFFER_MULTIPLIER` - defaults to `1.5`.

How much a deployment marked deprecated is down-weighted rather than excluded.

- `POINTSMAN_ROUTING_DEPRECATED_WEIGHT_PENALTY` - defaults to `0.5`.

## BANDIT (outcome-learning routing)

The bandit is ON in this stack: the learner is right here in the compose. The bare-jar default is off, because a gateway without a learner beside it should not pretend to learn.

- `POINTSMAN_BANDIT_ENABLED` - defaults to `true`.

Whether a request routed to ONE concrete model (named by the caller, or the key or tenant default) teaches the bandit groups that contain that model. off (default): it teaches nothing. observe: its cost, latency and whether it returned are recorded for every strategy: bandit group with that exact provider:model member, marked provenance direct. Never quality: a direct request gets no decision id, so no rating attaches to it. Callers pick models for their hard prompts, so what is learned carries that selection.

- `POINTSMAN_BANDIT_LEARN_FROM_DIRECT` - defaults to `off`.

The in-process cost sampler (objective: cost groups and quality profiles with selection: cost) keeps per-tenant state in memory. It is bounded: beyond max-scopes the least recently used tenant/group state is dropped, which only returns that group to declared prices; an arm beyond max-arms-per-scope is ranked from its declared price and not learned; label scopes (the feedback floor's memory) are never dropped, and a new one beyond max-label-scopes is refused and counted. Gauges: pointsman_cost_sampler_scopes, _arms, _label_scopes; drops: pointsman_cost_sampler_evictions_total.

- `POINTSMAN_COST_SAMPLER_MAX_SCOPES` - defaults to `10000`.
- `POINTSMAN_COST_SAMPLER_MAX_ARMS_PER_SCOPE` - defaults to `64`.
- `POINTSMAN_COST_SAMPLER_MAX_LABEL_SCOPES` - defaults to `100000`.

The exploration floor, applied by the Thompson strategy (a LinTS group applies no floor). The router rolls once against this rate and, when the roll lands, picks uniformly among the candidates, so each arm's guaranteed share is the floor DIVIDED BY the candidate count, not the floor itself: at 0.05 with five arms a losing arm keeps about 1 percent of traffic. That share is what stops a losing arm being permanently abandoned, so a provider that improves can be noticed. It also sets how long a judged comparison takes to resolve. As an illustration under one assumed effect size and noise level, not a threshold: separating two arms at two sigma might need about 75 observations of the losing arm, which at a 1 percent share is about 7,500 requests.

- `POINTSMAN_BANDIT_EXPLORATION_FLOOR` - defaults to `0.05`.

How the reward is composed from latency, cost and quality.

- `POINTSMAN_BANDIT_COMPOSITION_ACTIVE` - defaults to `additive`.
- `POINTSMAN_BANDIT_REWARD_LATENCY_TARGET_MS` - defaults to `2000`.

THE QUALITY PREFERENCE - one knob, per task type. How much the answer's quality matters against what it consumed (latency, relative cost), 0.0 = pure efficiency, 1.0 = pure quality. Replaces the four internal reward weights as operator surface; the internal weights survive as ratios within a side. Default 0.3, cost-leaning by asymmetry: a too-cheap default gets noticed and dialed up, a too-expensive one is a bill nobody can undo. Out-of-range refuses at boot. The per-task overrides ride the same task types the sampling key already splits on (chat, code, reasoning, knowledge...), so a preference set for knowledge moves knowledge routing alone. Calibrated against the 6000-request corpus run of 2026-09-01 (offline recomposition of the 2000 knowledge-key observations; mean composed reward per arm - a fixed-data sweep, not a live-selection run). Re-derived after the length-is-not-quality ruling moved the crossover from 0.9 to 0.85: preference   deepseek (82.4% MMLU, ~0.9s)   grok (95.3% MMLU, ~3.5s, 11x price) 0.3          0.935  <- leader                0.508 0.5          0.903  <- leader                0.635 0.7          0.871  <- leader                0.762 0.85         0.848                           0.858  <- leader 0.9          0.840                           0.890  <- leader Read it as: thirteen points of MMLU accuracy against eleven times the price crosses over at a preference of 0.85. UNRATED traffic composes as pure efficiency at every preference (the answer side is empty until feedback or a judge speaks), so a high preference with no feedback routes exactly like a low one - and says so: the pointsman.bandit.quality_signal.rated_fraction gauge (per task) tracks how much traffic carries feedback, and a WARN fires once per transition when a task's preference exceeds 0.5 with zero rated requests across a 200-event window (both constants compiled-in: the fix is sending feedback, not tuning the alarm). A late rating (learning semantics ts2, 17 Oct 2026) REVISES its decision's reward with the rating as the explicit component, weighted by this same knob; the decision stays one unit of evidence at every preference, so the anti-poisoning ceiling is FEEDBACK_RPM_LIMIT decisions revised per key per minute. (Under the legacy ts1 semantics a rating was p/(1-p) extra observations; that holds only for decisions sealed before the change.)

- `POINTSMAN_BANDIT_REWARD_QUALITY_PREFERENCE` - defaults to `0.3`.

Per-task overrides as ONE delimited string - "knowledge=0.9,code=0.7" - because an environment variable cannot address a map key through Spring's relaxed binding (the per-key spelling bound from yaml and silently ignored env, proven at boot 2026-09-01).

- `POINTSMAN_BANDIT_REWARD_QUALITY_PREFERENCE_BY_TASK` - defaults to empty.

How often the gateway polls the learner for published posteriors (the poll itself has a fixed 2 s fetch timeout), and the budget for a reward POST, multiplied by four inside the learner client's breaker. The gateway samples in-process, so a slow or dead learner costs freshness, never latency.

- `POINTSMAN_BANDIT_PARAMS_POLL_MS` - defaults to `10000`.
- `POINTSMAN_BANDIT_TIMEOUT_MS` - defaults to `50`.

The learner client's breaker: the failure rate over its five-call window that opens it, and how long it stays open before probing. While open, the gateway keeps routing from held posteriors (invariant 1); this only decides how quickly parameter polls and reward posts stop being attempted against a sick learner, and how soon they are tried again. Compiled in until 2026-09-02.

- `POINTSMAN_BANDIT_BREAKER_FAILURE_RATE_PERCENT` - defaults to `50`.
- `POINTSMAN_BANDIT_BREAKER_OPEN_SECONDS` - defaults to `10`.
- `POINTSMAN_HEALTH_POLICY_PROBE_TIMEOUT_MS` - defaults to `400`.

Minimum evidence per arm before learned weights override the configured ones. Below this gate every arm keeps its yaml weight.

- `POINTSMAN_BANDIT_LEARNED_WEIGHTS_MIN_PULLS` - defaults to `30`.

LinTS exploration scale. Larger explores more.

- `POINTSMAN_BANDIT_LINTS_V` - defaults to `0.5`.

The reward wire. Rewards arrive seconds to minutes after the decision, so they queue; these bound the queue, the retries and the shutdown drain.

- `POINTSMAN_BANDIT_REWARD_QUEUE_SIZE` - defaults to `1000`.
- `POINTSMAN_BANDIT_REWARD_RETRY_ATTEMPTS` - defaults to `3`.
- `POINTSMAN_BANDIT_REWARD_RETRY_BACKOFF_MS` - defaults to `250`.
- `POINTSMAN_BANDIT_REWARD_DRAIN_TIMEOUT_MS` - defaults to `5000`.

How long a decision is remembered so a late reward can find it, and how many such decisions are held at once.

- `POINTSMAN_BANDIT_DECISION_MEMORY_TTL_MIN` - defaults to `15`.
- `POINTSMAN_BANDIT_DECISION_MEMORY_MAX` - defaults to `50000`.

How much per-request feedback one caller may send.

- `POINTSMAN_BANDIT_FEEDBACK_RPM_LIMIT` - defaults to `5`.

The window that limit counts over, and the Retry-After a refused caller is told: one number (a literal minute and a literal "60" until 2026-09-02).

- `POINTSMAN_BANDIT_FEEDBACK_WINDOW_SECONDS` - defaults to `60`.

## SEMANTIC CACHE

Whether a hit extends the entry's life (sliding TTL), and the absolute cap on how long repeated hits may keep an **L2** entry warm. The max-age cap does not apply to L1; L1 follows its configured sliding or fixed TTL, including exact replays promoted from L2.

- `POINTSMAN_CACHE_REFRESH_ON_HIT` - defaults to `true`.
- `POINTSMAN_CACHE_MAX_AGE_HOURS` - defaults to `168`.

Streaming responses: served from cache at all, how the replay is chunked, and the inter-chunk pace that keeps a replay from arriving as one burst. ENABLED gates the cache read and write-through only: a streamed request is ledgered, reconciled and rewarded the same with it off.

- `POINTSMAN_CACHE_STREAMING_ENABLED` - defaults to `true`.
- `POINTSMAN_CACHE_STREAMING_REPLAY` - defaults to `single`.
- `POINTSMAN_CACHE_STREAMING_PACE_DELAY_MS` - defaults to `8`.

The lexical verifier's overlap floor. This is the false-positive guard that sits behind the vector match, not the vector threshold itself - the threshold is runtime-tunable through the admin surface.

- `POINTSMAN_CACHE_VERIFICATION_MIN_OVERLAP` - defaults to `0.4`.

REFUSE TO START rather than serve a lexical cache. The embedder falls back to lexical hashing when the MiniLM model cannot load, which is announced loudly but still runs; set true where a false positive would be worse than an outage.

- `POINTSMAN_CACHE_REQUIRE_SEMANTIC` - defaults to `false`.

Local embedder artifacts. Blank downloads on first use.

- `POINTSMAN_CACHE_MINILM_MODEL_URI` - defaults to empty.
- `POINTSMAN_CACHE_MINILM_TOKENIZER_URI` - defaults to empty.

ONNX workers spin only during active inference, then park when the last call returns. Set ALLOW_SPINNING=false for fully blocking workers; FORCE_SPINNING_STOP=false with

- `ALLOW_SPINNING` - defaults to `true restores unrestricted spinning. Model arithmetic is unchanged.`.
- `POINTSMAN_CACHE_MINILM_ALLOW_SPINNING` - shipped as `true`.
- `POINTSMAN_CACHE_MINILM_FORCE_SPINNING_STOP` - shipped as `true`.

0 uses min(4, half JVM container CPUs), at least one, reserving CPU for other stages. Positive values explicitly size the worker pool.

- `POINTSMAN_CACHE_MINILM_INTRA_OP_THREADS` - shipped as `0`.

Optional native window checks: UTF-16 input ceiling and concurrent calls per embedder. Positive integers. Oversized/busy checks bypass semantic caching, not provider requests.

- `POINTSMAN_CACHE_MINILM_MAX_CHECK_CHARACTERS` - shipped as `16384`.
- `POINTSMAN_CACHE_MINILM_MAX_CONCURRENT_WINDOW_CHECKS` - shipped as `2`.

A hosted embedder instead of the local MiniLM, for an operator running a larger embedding model behind an OpenAI-compatible /v1/embeddings endpoint. Selected with POINTSMAN_CACHE_EMBEDDER=hosted. Three settings are then REQUIRED and the gateway refuses to start without any of them: base-url, model and max-input-tokens. api-key-env is optional; it names the variable holding the key, never the key itself, and a named-but-empty variable only warns (calls go out unauthenticated). timeout-ms has a real default. max-input-tokens has no safe default because no embeddings API discloses its context window: declare the provider's real input limit and the cache skips prompts longer than it. Declaring MORE than the real limit lets the provider truncate silently, which is worse than skipping. The window is part of the L2 fingerprint, so changing it starts the semantic tier cold. Details: docs/semantic-cache.md, "Hosting your own embedding model".

- `POINTSMAN_CACHE_HOSTED_BASE_URL` - no default; required once the feature above is selected, and the gateway refuses to start without it. Ignored otherwise.
- `POINTSMAN_CACHE_HOSTED_MODEL` - no default; required once the feature above is selected, and the gateway refuses to start without it. Ignored otherwise.
- `POINTSMAN_CACHE_HOSTED_API_KEY_ENV` - defaults to empty.
- `POINTSMAN_CACHE_HOSTED_MAX_INPUT_TOKENS` - no default; required once the feature above is selected, and the gateway refuses to start without it. Ignored otherwise.
- `POINTSMAN_CACHE_HOSTED_TIMEOUT_MS` - defaults to `2000`.

How often a hosted embedder that failed its probe tries again (it starts NOT READY and the semantic tier misses until it answers). Compiled in at 30s until 2026-09-02.

- `POINTSMAN_CACHE_HOSTED_REPROBE_MS` - defaults to `30000`.

Redis limits for the cache instance. Blank leaves the server's own configuration alone.

- `POINTSMAN_CACHE_REDIS_MAXMEMORY` - defaults to empty.
- `POINTSMAN_CACHE_REDIS_MAXMEMORY_POLICY` - defaults to empty.

The Redis tier's bound (vector-backend=redis). New L2 writes stop while the index holds MAX_DOCUMENTS or more, checked every SIZE_CHECK_MS - a soft threshold: writes admitted since the last check, including writes queued on other replicas, can still land, so the cache Redis's maxmemory is the hard bound. STORE_CHECKS=enforce also stops them unless the cache Redis has maxmemory and an allkeys-* eviction policy; report only shows it in health. Run the cache on its own Redis: budget counters need noeviction, so never give a Redis that holds them an eviction policy.

- `POINTSMAN_CACHE_L2_MAX_DOCUMENTS` - defaults to `200000`.
- `POINTSMAN_CACHE_L2_SIZE_CHECK_MS` - defaults to `10000`.

How many CONSECUTIVE unreadable document counts are tolerated before L2 writes close. The count is an FT.SEARCH against the index and can exceed the op timeout on a busy cache store; closing writes on one such sample costs a whole check interval for a momentary read failure. Tolerating a few is bounded twice: by this count, and by requiring the last count that WAS read to sit below 90 percent of max-documents. Zero restores the previous behaviour of closing on the first unreadable count.

- `POINTSMAN_CACHE_L2_SIZE_UNKNOWN_TOLERANCE` - defaults to `3`.
- `POINTSMAN_CACHE_L2_STORE_CHECKS` - defaults to `report`.

Threads for the gateway's periodic jobs (cache write admission, budget store checks, reconcilers, parameter polls). At least 2: jobs on one thread wait for each other, so a slow job used to delay the checks that decide admission.

- `POINTSMAN_SCHEDULING_POOL_SIZE` - defaults to `4`.
- `POINTSMAN_CACHE_L2_OP_TIMEOUT_MS` - defaults to `50`.

The COLD-PATH budget, deliberately separate from the request-path one above: creating the RediSearch index and setting the eviction policy happen once, on a JIT-cold JVM, and 50ms is the number for a lookup that must not delay a request. Sharing it made the gateway warn that Redis was unreachable when Redis was fine.

- `POINTSMAN_CACHE_L2_BOOTSTRAP_TIMEOUT_MS` - defaults to `5000`.

The bounded queue behind the L2 write-through: a miss's vector and body are stored off the request thread, and a full queue DROPS the write (a lost cache entry, never a slow response) and counts it. The query-vector memo that lets that store reuse the read path's embedding is DERIVED from this number (one and a half times it), so raising the queue can never outgrow the memo. This key was settable but undiscoverable until 2026-09-02.

- `POINTSMAN_CACHE_L2_ASYNC_STORE_QUEUE` - defaults to `1000`.

Candidates asked of the vector index per semantic lookup, before the threshold and the verifier narrow them. More costs index time per miss and gives the verifier more to choose from; fewer is cheaper and blinder. Compiled in at 10 until 2026-09-02.

- `POINTSMAN_CACHE_L2_QUERY_TOP_K` - defaults to `10`.

The L2 breaker: the failure rate over its window (20 calls, 10 minimum; the window shape stays compiled in as the breaker's statistics) that opens it, and how long it stays open. Open means L2 is skipped entirely (lookup misses, store drops) so a sick Redis costs nothing per request. Compiled in until 2026-09-02.

- `POINTSMAN_CACHE_L2_BREAKER_FAILURE_RATE_PERCENT` - defaults to `50`.
- `POINTSMAN_CACHE_L2_BREAKER_OPEN_SECONDS` - defaults to `5`.

How long a request waits behind another that is already computing the same answer, before giving up and calling the provider itself.

- `POINTSMAN_CACHE_SINGLE_FLIGHT_FOLLOWER_TIMEOUT_MS` - defaults to `60000`.

Multi-turn conversation condensing, via the policy service.

- `POINTSMAN_CACHE_CONTEXT_TIMEOUT_MS` - defaults to `2000`.

Durable hit samples for tuning the threshold. debug-sample-content stores the PROMPT TEXT of sampled hits: off by default, and a startup warning says so when you turn it on.

- `POINTSMAN_CACHE_DEBUG_SAMPLE_RATE` - defaults to `0.02`.
- `POINTSMAN_CACHE_DEBUG_SAMPLE_CONTENT` - defaults to `false`.

## GUARDRAILS

The whole chain's master switch, and the share of verdicts written to the audit log.

- `POINTSMAN_GUARDRAILS_ENABLED` - defaults to `true`.
- `POINTSMAN_GUARDRAILS_AUDIT_SAMPLE_RATE` - defaults to `1.0`.

Whether an X-Guardrails request header may skip named rules. Off means never, which is the right answer unless every caller is trusted.

- `POINTSMAN_GUARDRAILS_ALLOW_REQUEST_OVERRIDE` - defaults to `false`.

Whether guardrail verdicts are echoed in the response body.

- `POINTSMAN_GUARDRAILS_EXPOSE_RESULTS_IN_BODY` - defaults to `false`.

The four rules that ship enabled. Each takes enabled and mode (enforce | monitor); the two count rules also take a ceiling. Every other rule ships disabled and is configured through the admin API. The ReDoS budget both regex rules (blocked-pattern, response-blocked- pattern) apply: char accesses one pattern may spend on one message before it is declared a probable ReDoS and SKIPPED for that message. The skip is fail-open for that pattern, logged at WARN, and not counted anywhere (a filed finding). Raise it if a legitimate pattern on long messages is being skipped; lower it to bound worst-case scan time harder. Compiled in at five million until 2026-09-02.

- `POINTSMAN_GUARDRAILS_REGEX_BUDGET_CHARS` - defaults to `5000000`.

- `POINTSMAN_GUARDRAILS_RULES_SECRETS_ENABLED` - defaults to `true`.
- `POINTSMAN_GUARDRAILS_RULES_SECRETS_MODE` - defaults to `enforce`.
- `POINTSMAN_GUARDRAILS_RULES_PROMPT_INJECTION_HEURISTIC_ENABLED` - defaults to `true`.
- `POINTSMAN_GUARDRAILS_RULES_PROMPT_INJECTION_HEURISTIC_MODE` - defaults to `monitor`.
- `POINTSMAN_GUARDRAILS_RULES_MAX_MESSAGES_ENABLED` - defaults to `true`.
- `POINTSMAN_GUARDRAILS_RULES_MAX_MESSAGES_MODE` - defaults to `enforce`.
- `POINTSMAN_GUARDRAILS_RULES_MAX_MESSAGES_MAX` - defaults to `200`.
- `POINTSMAN_GUARDRAILS_RULES_MAX_RESPONSE_TOKENS_ENABLED` - defaults to `true`.
- `POINTSMAN_GUARDRAILS_RULES_MAX_RESPONSE_TOKENS_MODE` - defaults to `enforce`.
- `POINTSMAN_GUARDRAILS_RULES_MAX_RESPONSE_TOKENS_MAX` - defaults to `32768`.

PII detection engine: the built-in regex, or a Presidio service you run. Selecting presidio makes the analyzer URL REQUIRED: the gateway refuses to start with engine=presidio and no url, rather than quietly falling back to regex while you believe Presidio is classifying. api-key is optional. Presidio unreachable under enforce fails CLOSED.

- `POINTSMAN_GUARDRAILS_PII_ENGINE` - defaults to `regex`.
- `POINTSMAN_GUARDRAILS_PII_PRESIDIO_URL` - no default; required once the feature above is selected, and the gateway refuses to start without it. Ignored otherwise.
- `POINTSMAN_GUARDRAILS_PII_PRESIDIO_API_KEY` - defaults to empty.
- `POINTSMAN_GUARDRAILS_PII_PRESIDIO_API_KEY_HEADER` - defaults to `X-API-Key`.
- `POINTSMAN_GUARDRAILS_PII_PRESIDIO_TIMEOUT_MS` - defaults to `3000`.

## RATE LIMITS AND BUDGET

The RPM/TPM window algorithm. Budget is always fixed-window. What the default means for a caller: a token bucket whose CAPACITY is the limit's count (an RPM of 600 is a bucket of 600 requests; a TPM of 100000 is a bucket of 100000 tokens), REFILLED continuously at count per window (600 per minute is 10 per second, credited fractionally per millisecond), and a NEW bucket STARTS FULL. So the burst a caller can push through is the whole count at once from idle, after which admission runs at the refill rate; a caller who pauses for one full window has the whole burst back. TPM buckets consume the ESTIMATED tokens at admission and are reconciled to real usage at the exit (a capped refund or an extra debit). The alternatives: sliding-window (at most count per rolling window, estimated from the current and previous window's counters, so no burst beyond count and two Redis keys per limit) and fixed-window (count per tumbling window; up to 2x count can pass across a boundary). Budget windows are calendar windows and never bucket.

- `POINTSMAN_RATELIMIT_STRATEGY` - defaults to `token-bucket`.

The chars-per-token divisor behind every character-based token estimate: the routing profile, TPM admission's prompt count and its tool-definition twin, the bandit's prompt-length feature, and the charge for a stream that ended without usage. ONE number on purpose: the admission ruling needs the message count and the tool count wrong in the same direction. Raising it shrinks every estimate (fewer up-front refusals, more over-admission); lowering it inflates them. Reservations reconcile against the provider's real usage at the exit, so this decides admission - except for a stream that ended without usage, whose settled charge IS this estimate (integer division; a very short reply can estimate zero output tokens). The hosted embedder's window guard keeps its own divisor: that bounds the embedding provider's tokenizer, a different thing. Was a literal / 4 in four places until 2026-09-02.

- `POINTSMAN_ESTIMATE_CHARS_PER_TOKEN` - defaults to `4`.

What TPM admission reserves for output size when the caller sends no max_tokens - AND the max_tokens the gateway sends to Anthropic in that case, because Anthropic requires one and what is allowed must be what was reserved (it was 4096 sent against 512 reserved, an eightfold over-admission, until 2026-09-02). Over-estimating is the safe direction for an admission gate: the reservation is corrected to true usage at the exit. Callers who want a smaller reservation send max_tokens.

- `POINTSMAN_RATELIMIT_TPM_DEFAULT_OUTPUT_ESTIMATE` - defaults to `4096`.

Image inputs cost hard-to-predict tokens, so they price at this multiple before the call. Over-estimating is again the safe direction for a money gate.

- `POINTSMAN_BUDGET_IMAGE_ESTIMATE_MULTIPLIER` - defaults to `1.5`.

A flat fallback price used only where no per-model pricing is known.

- `POINTSMAN_RATELIMIT_BUDGET_PRICE_PER_1K_TOKENS` - defaults to `0.0`.

How long an in-flight concurrency slot may be held before it is reclaimed as leaked.

- `POINTSMAN_RATELIMIT_CONCURRENCY_MAX_DURATION_MS` - defaults to `600000`.

The counter store. Off runs the limiter in-process, which is correct for a single instance and wrong for several.

- `POINTSMAN_REDIS_ENABLED` - defaults to `true`.

The counter-store breaker in front of every Redis counter call: consecutive health failures (timeouts, connection loss) that open it, and how long it fast-fails before admitting one probe. Open means rate limits fail open and budgets fail closed through the ledger; this decides how fast a sick Redis stops costing every request its command timeout. Compiled in at 5 / 10s until 2026-09-02; the window is 1s since v1 qualification (26 Sep 2026), because with budgets failing closed it is how long capped traffic is refused after a stall.

- `POINTSMAN_REDIS_BREAKER_FAILURE_THRESHOLD` - defaults to `5`.
- `POINTSMAN_REDIS_BREAKER_OPEN_MS` - defaults to `1000`.

The total /v1 requests in flight. Virtual threads removed the accidental brake a fixed thread pool used to provide, so this bound is explicit.

- `POINTSMAN_INTAKE_MAX_IN_FLIGHT` - defaults to `256`.

The billing ledger. Operator-owned: a tenant cannot shorten it.

- `POINTSMAN_RETENTION_USAGE_EVENTS_DAYS` - defaults to `730`.

The routing decision log, and the ceiling a tenant's own setting is capped at.

- `POINTSMAN_DECISION_LOG_RETENTION_DAYS` - defaults to `30`.

Guardrail events, by kind. Compliance events (a PII or secrets block) outlive the merely degraded and the flagged.

- `POINTSMAN_RETENTION_GUARDRAIL_COMPLIANCE_DAYS` - defaults to `365`.
- `POINTSMAN_RETENTION_GUARDRAIL_DEGRADED_DAYS` - defaults to `90`.
- `POINTSMAN_RETENTION_GUARDRAIL_FLAG_DAYS` - defaults to `90`.

Cache hit samples kept for threshold tuning.

- `POINTSMAN_RETENTION_CACHE_SAMPLES_DAYS` - defaults to `30`.

The per-tenant default and the ceiling a tenant may not exceed.

- `POINTSMAN_RETENTION_TENANT_DEFAULT_DAYS` - defaults to `365`.
- `POINTSMAN_RETENTION_TENANT_MAX_DAYS` - defaults to `3650`.

Count what WOULD be deleted and delete nothing. Turn this on first when changing any number above.

- `POINTSMAN_RETENTION_REPORT_ONLY` - defaults to `false`.

Rows per delete batch. Must be positive: a batch of zero would loop the nightly purge forever, so the gateway refuses to boot on it.

- `POINTSMAN_RETENTION_BATCH_SIZE` - defaults to `10000`.

Quality evidence (quality-guarded routing). OFF by default: nothing is deleted until an owner chooses lifetimes (decision D3-2); the row-count gauge pointsman_quality_retention_rows shows growth either way. When on, events of a decision an audit job selected and unresolved auxiliary attempts (RESERVED, UNKNOWN) are never deleted. A run deletes at most max-batches x batch-size rows per table.

- `POINTSMAN_QUALITY_RETENTION_ENABLED` - defaults to `false`.
- `POINTSMAN_QUALITY_RETENTION_EVENTS_DAYS` - defaults to `365`.
- `POINTSMAN_QUALITY_RETENTION_AUX_ATTEMPTS_DAYS` - defaults to `730`.
- `POINTSMAN_QUALITY_RETENTION_AUDIT_DROPS_DAYS` - defaults to `90`.
- `POINTSMAN_QUALITY_RETENTION_BATCH_SIZE` - defaults to `1000`.
- `POINTSMAN_QUALITY_RETENTION_MAX_BATCHES` - defaults to `100`.
- `POINTSMAN_QUALITY_RETENTION_CRON` - defaults to `0 20 4 * * *`.

## AUTHENTICATION AND KEYS

Whether /v1 requires a virtual key. Off is for local experiments only.

- `POINTSMAN_AUTH_REQUIRE_VIRTUAL_KEY` - defaults to `true`.

How long a rotated key's predecessor keeps working, so callers can migrate without an outage.

- `POINTSMAN_AUTH_ROTATION_DEFAULT_GRACE_HOURS` - defaults to `24`.

Whether a successor key inherits the retired key's model allow-list.

- `POINTSMAN_LIFECYCLE_SUCCESSOR_INHERITS_ALLOWLIST` - defaults to `true`.

Admin login throttling.

- `POINTSMAN_AUTH_LOGIN_THROTTLE_MAX_ATTEMPTS` - defaults to `10`.
- `POINTSMAN_AUTH_LOGIN_THROTTLE_WINDOW_MINUTES` - defaults to `15`.

How coarsely a key's last-used timestamp is written. Longer means fewer writes and a staler figure.

- `POINTSMAN_AUTH_LAST_USED_WINDOW` - defaults to `PT1H`.

Maximum scope nesting below a tenant.

- `POINTSMAN_SCOPES_MAX_DEPTH_BELOW_TENANT` - defaults to `2`.

## OBSERVABILITY AND THE DECISION LOG

Share of routing decisions written durably. Below 1.0 the dashboard's per-arm reward panels thin out proportionally.

- `POINTSMAN_DECISION_LOG_SAMPLE_RATE` - defaults to `1.0`.
- `POINTSMAN_DECISION_LOG_QUEUE_SIZE` - defaults to `2048`.
- `POINTSMAN_DECISION_LOG_DRAIN_TIMEOUT_MS` - defaults to `5000`.

Cap on prompt/response bytes attached to a trace span when content capture is on.

- `POINTSMAN_OBSERVABILITY_OTEL_CAPTURE_CONTENT_MAX_BYTES` - defaults to `2048`.

Allows POST /actuator/env, which is how the two runtime-tunable settings (the cache match threshold, the context and temperature modes) are changed without a restart. It is a write endpoint behind admin auth; the gateway warns at startup when it is on. It ships ON, because turning it off makes those two settings restart-only.

- `MANAGEMENT_ENDPOINT_ENV_POST_ENABLED` - defaults to `true`.

## STREAMING

The overall SSE connection lifetime, and the idle gap after which a stalled stream is abandoned.

- `POINTSMAN_STREAMING_SSE_TIMEOUT_MS` - defaults to `300000`.

How long a shutdown waits, AFTER the web server has drained and stopped, for the requests it had accepted to finish settling - synchronous calls still running and streams still open or settling (budget reconcile, journal write) - before the context closes the stores under them. Past it the open work is reported and counted; boot recovery turns each request's intent on disk into one 503 accounting_interrupted row with UNKNOWN usage and zero cost - the final charge is not recovered, and the budget reservation stays on the counter until its window ends.

- `POINTSMAN_STREAMING_SETTLEMENT_SHUTDOWN_WAIT_MS` - defaults to `30000`.

RESERVATION MANIFEST (recovery slices S2-S3): when true, every cost reservation writes counter_reservation rows in line with admission (PREPARED before the reserve, ADMITTED or REFUSED after it) and usage_charges rows at settlement. Off in the evaluation stack; ON in the v1 release profile (docker/docker-compose.v1.yml) together with the reconcile applier and transaction-first settlement below, which use these rows to apply each owed counter adjustment exactly once. The tables are created by the gateway on start-up. Recovery after a counter-store loss remains a manual attestation. Requires the durable usage journal (POINTSMAN_USAGE_JOURNAL_DIRECTORY): with the manifest on, the journal's settled record is what replays a settlement whose transaction failed; the gateway refuses to boot with the manifest on and the journal off (slice S3).

- `POINTSMAN_BUDGET_MANIFEST_ENABLED` - defaults to `false`.

Retention for the manifest tables (slice S3, design section 8): terminal, unreferenced rows (REFUSED, RELEASED, and SETTLED rows whose adjustment demonstrably happened) leave once their period ended this many days ago; unresolved work never ages out. Bounded batches, one instance nightly.

- `POINTSMAN_BUDGET_MANIFEST_ROW_RETENTION_DAYS` - defaults to `35`.
- `POINTSMAN_BUDGET_MANIFEST_RETENTION_BATCH_SIZE` - defaults to `5000`.
- `POINTSMAN_BUDGET_MANIFEST_RETENTION_CRON` - defaults to `0 50 3 * * *`.

RESERVATION MARKER AND RESOLVER (design 6a, R2): with the manifest on, each reservation records what it applied under a short-lived marker in the counter store, so a reserve whose reply was lost is resolved instead of holding budget to the end of the period. The resolver (active with the reconcile applier) gives back a held reservation exactly once, or tombstones one that never applied, but only for a request the ledger shows finished without reaching a provider; anything else stays held and is counted (pointsman_budget_reservation_unresolved) for the recovery procedure. A missing marker is read as "never applied" only inside half its lifetime. It also finishes a release whose counter-store call failed: a release records its intent (RELEASED, release_pending) before the counter moves, and a still-pending intent older than the delay is released through the marker.

- `POINTSMAN_BUDGET_MANIFEST_RESERVE_MARKER_TTL_MS` - defaults to `3600000`.
- `POINTSMAN_BUDGET_MANIFEST_RESERVE_RESOLVE_DELAY_MS` - defaults to `30000`.
- `POINTSMAN_BUDGET_MANIFEST_RESERVE_RESOLVE_INTERVAL_MS` - defaults to `15000`.
- `POINTSMAN_BUDGET_MANIFEST_RESERVE_RESOLVE_BATCH` - defaults to `200`.

RECONCILE APPLY (slice S4a): the owed-adjustment applier, driven from OWED rows of the manifest and nothing else. OFF by default (ON in the v1 release profile); when on it also requires the manifest, and it refuses a Redis Cluster endpoint (qualified on a single node only). LEGACY rows are never applied. The dedupe lifetime and bucket width are FIXED per operation at its creation and stored with it (a change applies to new operations only); lease-ms is how long a worker owns a claimed operation before another may take it; max-attempts bounds transient retries before an operation is PRESERVED and its tag fenced. Only calls that reached the counter store count: a call the open breaker refused is no attempt, and a store failure ends the pass with the rest of the batch unattempted.

- `POINTSMAN_BUDGET_RECONCILE_APPLY_ENABLED` - defaults to `false`.
- `POINTSMAN_BUDGET_RECONCILE_DEDUPE_LIFETIME_MS` - defaults to `3600000`.
- `POINTSMAN_BUDGET_RECONCILE_MAX_ATTEMPTS` - defaults to `64`.
- `POINTSMAN_BUDGET_RECONCILE_LEASE_MS` - defaults to `30000`.
- `POINTSMAN_BUDGET_RECONCILE_BATCH` - defaults to `100`.
- `POINTSMAN_BUDGET_RECONCILE_RETRY_INTERVAL_MS` - defaults to `1000`.

TRANSACTION FIRST (slice S4b): the settlement transaction writes each counter's adjustment as an OWED operation, and the applier runs it AFTER that transaction committed - the order the recovery design requires (a settlement is never applied to the counter store before its durable evidence exists). Requires the manifest AND the applier; refused at boot otherwise. Off, the adjustment runs first as it always has and the rows record LEGACY_<outcome>. ON in the v1 release profile (docker/docker-compose.v1.yml).

- `POINTSMAN_BUDGET_RECONCILE_TRANSACTION_FIRST` - defaults to `false`.

The cache replayer's emitter reads the same value since 2026-09-02; before that its five minutes were a separate literal this setting could not reach. Outbound timeouts for every dynamically-built provider client. A provider row's own timeout_ms always wins; default-timeout-ms is the read timeout for a row that declares none (the sync client's socket read, and the streaming client's total). connect-timeout-ms is the sync client's TCP connect budget; the streaming client's first-byte budget is POINTSMAN_INTAKE_FIRST_CHUNK_TIMEOUT_MS, a different thing. Both were literals inside the client builder until 2026-09-02.

- `POINTSMAN_PROVIDERS_DEFAULT_TIMEOUT_MS` - defaults to `60000`.
- `POINTSMAN_PROVIDERS_CONNECT_TIMEOUT_MS` - defaults to `10000`.
- `POINTSMAN_STREAMING_STREAM_IDLE_TIMEOUT_MS` - defaults to `45000`.

- `POINTSMAN_IDENTITY_L1_MAX_SIZE` - defaults to `50000`.
- `POINTSMAN_IDENTITY_L1_TTL_SECONDS` - defaults to `10`.
- `POINTSMAN_IDENTITY_L1_NEGATIVE_TTL_SECONDS` - defaults to `3`.
- `POINTSMAN_IDENTITY_L2_META_TTL_SECONDS` - defaults to `60`.
- `POINTSMAN_IDENTITY_L2_INVALID_TTL_SECONDS` - defaults to `5`.
- `POINTSMAN_IDENTITY_TTL_JITTER_PCT` - defaults to `0.2`.

Whether the new authorization core decides, or only shadows the old gates.

- `POINTSMAN_AUTHZ_CORE_DECIDES` - defaults to `true`.
- `POINTSMAN_SHADOW_AUTHZ_ENABLED` - defaults to `true`.
- `POINTSMAN_SHADOW_ALLOW_ENABLED` - defaults to `false`.

Whether the scope tree decides the data path.

- `POINTSMAN_DATAPATH_TREE_ENABLED` - defaults to `false`.

A canonical registry document imported on first boot into empty registry tables. Blank means the registry starts empty.

- `POINTSMAN_REGISTRY_SEED_FILE` - defaults to empty.

How often each instance compares its control-plane snapshots (limit rules, policy values, guardrail settings) against the control_version row - the missed-message backstop behind control.changed (multi-instance M-1).

- `POINTSMAN_CONTROL_RECONCILE_INTERVAL_MS` - defaults to `60000`.

Data-plane body ceiling, before JSON parsing (also applies to chunked uploads).

- `POINTSMAN_INTAKE_MAX_BODY_BYTES` - shipped as `1048576`.

estimate (fast, default) or tokenizer (warmed explicit model-row encoding, text only). Tokenizer mode counts the model or group the request RESOLVES to under the key's resolution policy (an unknown or omitted name served by fallback-to-allowed or a key default is counted with the serving model's encoding), so every model row a request can be served by needs capabilities.token_encoding=cl100k_base or o200k_base; a resolved row or group member without one is refused 400 unsupported_request_control, and a fallback rung whose encoding the request was not counted with is skipped. Chat framing and tool scaffolding remain estimated.

- `POINTSMAN_TOKENS_MODE` - shipped as `estimate`.

Durable usage delivery (requires a persistent directory unique to this gateway instance). Empty retains legacy best-effort async delivery. In Compose, /data/usage enables the mounted journal.

- `POINTSMAN_USAGE_JOURNAL_DIRECTORY` - no default; you supply this.
- `POINTSMAN_USAGE_JOURNAL_MAX_ENTRIES` - shipped as `10000`.

Refuse to start unless the journal directory is on a persistent volume of its own - not the container's root filesystem, not tmpfs/ramfs/overlay (read from /proc/self/mountinfo). The v1 overlay sets it true: without the volume the journal silently lived in the container's writable layer and was lost with it.

- `POINTSMAN_USAGE_JOURNAL_REQUIRE_VOLUME` - defaults to `false`.

Operational resource budgets and timing (restart gateway after changes). Positive integers unless noted. Defaults preserve the launch-readiness settings. Maximum entry-equivalent count; the per-entry weight floor enforces it.

- `POINTSMAN_CACHE_L1_MAXSIZE` - defaults to `10000`.

L1 TTL in minutes; refresh-on-hit determines sliding versus fixed expiry.

- `POINTSMAN_CACHE_L1_TTLMINUTES` - defaults to `15`.

Estimated payload ceiling in bytes (64 MiB), not a measured JVM heap limit.

- `POINTSMAN_CACHE_L1_MAXWEIGHTBYTES` - defaults to `67108864`.

Minimum weight per entry. Effective budget=min(max-weight-bytes, max-size * this).

- `POINTSMAN_CACHE_L1_MINENTRYWEIGHTBYTES` - defaults to `4096`.

Estimated metadata bytes added to key/text UTF-16 bytes; zero is allowed.

- `POINTSMAN_CACHE_L1_ENTRYOVERHEADBYTES` - defaults to `1024`.

Maximum cumulative UTF-16 characters accepted while assembling one provider stream; overflow aborts the stream.

- `POINTSMAN_STREAMING_ASSEMBLY_MAX_CHARS` - defaults to `524288`.

Maximum distinct tool-call indices assembled in one stream; overflow aborts the stream.

- `POINTSMAN_STREAMING_ASSEMBLY_MAX_TOOL_CALLS` - defaults to `128`.

Concurrent tokenizer-mode requests per instance; CPU concurrency remains bounded.

- `POINTSMAN_TOKENS_MAX_CONCURRENT_TOKENIZATIONS` - defaults to `2`.

Maximum waiting tokenizer requests and wait budget in ms; either zero restores immediate refusal. Queue full/expired/interrupted returns 503 tokenizer_busy; no heuristic fallback.

- `POINTSMAN_TOKENS_MAX_WAITING_TOKENIZATIONS` - defaults to `16`.
- `POINTSMAN_TOKENS_ADMISSION_WAIT_MS` - defaults to `25`.

Maximum terminal journal records delivered per replay pass.

- `POINTSMAN_USAGE_JOURNAL_REPLAY_BATCH_SIZE` - defaults to `100`.

Delay after each journal replay pass and before its first pass, milliseconds.

- `POINTSMAN_USAGE_JOURNAL_REPLAY_INTERVAL_MS` - defaults to `1000`.

L7 journal namespace: bounded epoch directories; see docs/operations/journal-epochs.md. An epoch admits at most this many identities before rotation. Existing identities may settle.

- `POINTSMAN_USAGE_JOURNAL_EPOCH_IDENTITIES` - shipped as `1024`.

Includes OPEN, PREPARED, CLOSED, CREATING and failed-removal directories; 2..256.

- `POINTSMAN_USAGE_JOURNAL_MAX_RETAINED_EPOCHS` - shipped as `32`.

Age-based rotation is opportunistic when a durable prepared epoch exists.

- `POINTSMAN_USAGE_JOURNAL_EPOCH_MAX_AGE_MS` - shipped as `60000`.

Limits admission waiting for creation; DOES NOT cancel kernel filesystem I/O.

- `POINTSMAN_USAGE_JOURNAL_EPOCH_CREATE_WAIT_MS` - shipped as `250`.

Startup estimate ceiling: E * R * (3 + 2 * max-dispatches) * 320 bytes. This is an estimate, not a kernel/cgroup memory guarantee. Unknown/quarantined evidence is separate.

- `POINTSMAN_USAGE_JOURNAL_NAME_BUDGET_BYTES` - shipped as `268435456`.

Journal filesystem units (record write+fsync+rename+dir fsync, delete, move, replay read) running at once; 1..256. Bounds journal descriptors (about 2 per unit) and journal units blocked in the kernel, not the JDK carrier pool; waiters park in arrival order, never time out. Bounds journal units, not requests (see pointsman.intake.max-in-flight).

- `POINTSMAN_USAGE_JOURNAL_IO_CONCURRENCY` - defaults to `16`.

Units that may wait for a journal I/O permit before a NEW request identity is refused (503 accounting_unavailable, before any provider send); settlements and replay are never refused. 1..100000.

- `POINTSMAN_USAGE_JOURNAL_IO_ADMISSION_WAITING` - defaults to `64`.

Send each physical provider request's dispatch id upstream as X-Pointsman-Dispatch-Id (a random UUID with no tenant information), so provider-side logs join to provider_dispatches. false sends no header.

- `POINTSMAN_USAGE_DISPATCH_HEADER_ENABLED` - defaults to `true`.

Maximum conversation summaries held per instance; eviction only causes recomputation.

- `POINTSMAN_CACHE_CONTEXT_MEMO_MAX_SIZE` - defaults to `1000`.

Fixed conversation-summary memo lifetime, milliseconds.

- `POINTSMAN_CACHE_CONTEXT_MEMO_TTL_MS` - defaults to `120000`.

Redis SCAN work hint for cache clearing; a hint, not a guaranteed batch size.

- `POINTSMAN_CACHE_REDIS_SCAN_COUNT` - defaults to `500`.

Prometheus TCP connection timeout, milliseconds.

- `POINTSMAN_PROMETHEUS_CONNECT_TIMEOUT_MS` - defaults to `2000`.

Prometheus response read timeout, milliseconds.

- `POINTSMAN_PROMETHEUS_READ_TIMEOUT_MS` - defaults to `3000`.

Background learner parameter-fetch timeout, milliseconds; independent of poll cadence.

- `POINTSMAN_BANDIT_PARAMS_TIMEOUT_MS` - defaults to `2000`.

Reward timeout is existing POINTSMAN_BANDIT_TIMEOUT_MS times this multiplier.

- `POINTSMAN_BANDIT_REWARD_TIMEOUT_MULTIPLIER` - defaults to `4`.

Number of reward calls in the learner circuit-breaker sliding window.

- `POINTSMAN_BANDIT_BREAKER_WINDOW_SIZE` - defaults to `5`.

Number of permitted learner reward probes in half-open state.

- `POINTSMAN_BANDIT_BREAKER_HALF_OPEN_CALLS` - defaults to `3`.

Local feedback throttle key capacity; eviction can reset a local-only throttle. Use Redis for fleet limits.

- `POINTSMAN_BANDIT_FEEDBACK_WINDOW_MAX_ENTRIES` - defaults to `100000`.

Local failed-login username capacity; eviction forgets that username window. Size for exposed account cardinality.

- `POINTSMAN_AUTH_LOGIN_THROTTLE_MAX_ENTRIES` - defaults to `100000`.

Maximum local conversation pins; at capacity new pins are declined after expired pins are removed.

- `POINTSMAN_ROUTING_AFFINITY_MAX_ENTRIES` - defaults to `32768`.

Sliding conversation affinity lifetime, milliseconds; this does not extend provider-side prompt cache lifetime.

- `POINTSMAN_ROUTING_AFFINITY_TTL_MS` - defaults to `300000`.

Maximum dynamic provider/credential client entries; new entries at capacity are refused.

- `POINTSMAN_PROVIDERS_MODEL_CACHE_MAX_ENTRIES` - defaults to `64`.

Queued retired clients awaiting closure; saturation runs the close on the caller, never drops it.

- `POINTSMAN_PROVIDERS_CLOSE_QUEUE_CAPACITY` - defaults to `100`.

## Optional Jev task routing

Off by default. Enabling it exports accepted conversation text - system and developer messages included - to the configured TypeSafe endpoint for the tenants listed in ALLOWED_TENANTS; obtain their permission first, the export is the feature. Classification calls are operator-paid overhead: never charged to any tenant's budget or usage ledger, and a call whose usage the provider did not report is counted as unverified, never as free. Attempt, rate and concurrency bounds are per PROCESS (reset on restart, multiply with replicas). Task-to-model rankings are YAML (pointsman.jev.task-models); there is no default. Timeout and confidence defaults are initial settings, not measured guarantees. Full setup: docs/how-to/jev-routing.md.

- `POINTSMAN_JEV_ENABLED` - defaults to `false`.
- `POINTSMAN_JEV_ENDPOINT` - defaults to `https://api.typesafe.ai/v1/systemone`.
- `POINTSMAN_JEV_MODEL` - defaults to `jev-1.13.0`.
- `POINTSMAN_JEV_API_KEY` - defaults to empty.
- `POINTSMAN_JEV_TIMEOUT_MS` - defaults to `750`.
- `POINTSMAN_JEV_CONNECT_TIMEOUT_MS` - defaults to `250`.
- `POINTSMAN_JEV_MAX_CONCURRENT` - defaults to `4`.
- `POINTSMAN_JEV_REQUESTS_PER_MINUTE` - defaults to `600`.
- `POINTSMAN_JEV_MAX_CALLS_PER_PROCESS` - defaults to `10000`.
- `POINTSMAN_JEV_BREAKER_FAILURES` - defaults to `5`.
- `POINTSMAN_JEV_BREAKER_OPEN_MS` - defaults to `10000`.
- `POINTSMAN_JEV_MAX_INPUT_CHARACTERS` - defaults to `8000`.
- `POINTSMAN_JEV_MAX_MESSAGES` - defaults to `32`.
- `POINTSMAN_JEV_MAX_REQUEST_BYTES` - defaults to `16384`.
- `POINTSMAN_JEV_MAX_RESPONSE_BYTES` - defaults to `16384`.
- `POINTSMAN_JEV_MINIMUM_CONFIDENCE` - defaults to `0.8`.
- `POINTSMAN_JEV_PROBABILITY_TOLERANCE` - defaults to `0.000001`.
- `POINTSMAN_JEV_ALLOWED_TENANTS` - defaults to empty.

## Optional quality-guarded routing (EXPERIMENTAL)

Off by default. With it off there is no quality strategy, no policy snapshot and no background job, and a routing group that names strategy quality-guarded REFUSES startup instead of degrading. Every value is bounded and a value outside its bound refuses startup by name. There is no quality threshold here on purpose: thresholds belong to an evaluation spec for one task profile. Auxiliary spend (judge, audits, alternative answers, cascade stages) is operator-funded and never charged to a tenant; enabling any paid part requires an explicit account id and explicit limits. Full setup: docs/how-to/quality-guarded-routing.md.

- `POINTSMAN_QUALITY_ENABLED` - defaults to `false`.

How often the active policy is re-read into memory, ms [1000, 300000]; selection never waits for it.

- `POINTSMAN_QUALITY_SNAPSHOT_REFRESH_MS` - defaults to `10000`.

How long a quality receipt accepts late evidence, hours [1, 2160].

- `POINTSMAN_QUALITY_RECEIPT_TTL_HOURS` - defaults to `168`.

Evidence submissions per API key per minute, per instance [1, 6000].

- `POINTSMAN_QUALITY_SUBMISSIONS_PER_MINUTE` - defaults to `60`.

Longest opaque evidence reference accepted, characters [16, 2048]; a reference, never a payload.

- `POINTSMAN_QUALITY_MAX_EVIDENCE_REF_CHARS` - defaults to `256`.

Largest sealed evaluation batch, rows [1, 200000]; largest policy document, bytes [1024, 1048576].

- `POINTSMAN_QUALITY_MAX_BATCH_ROWS` - defaults to `20000`.
- `POINTSMAN_QUALITY_MAX_POLICY_BYTES` - defaults to `262144`.

Audit lane: fixed inclusion probability decided before the outcome [0, 1]; bounded queue [1, 10000], workers [1, 8], retries [0, 3]; finished jobs kept [1, 365] days. A full queue DROPS and counts.

- `POINTSMAN_QUALITY_AUDIT_ENABLED` - defaults to `false`.
- `POINTSMAN_QUALITY_AUDIT_SAMPLE_RATE` - defaults to `0.0`.
- `POINTSMAN_QUALITY_AUDIT_QUEUE_SIZE` - defaults to `256`.
- `POINTSMAN_QUALITY_AUDIT_WORKERS` - defaults to `1`.
- `POINTSMAN_QUALITY_AUDIT_RETRY_ATTEMPTS` - defaults to `1`.
- `POINTSMAN_QUALITY_AUDIT_RETENTION_DAYS` - defaults to `30`.

The operator-funded auxiliary account. Limits are FLEET-WIDE (one Postgres row, conditional reservations). Micro-USD: 1000000 = 1 USD. Each attempt reserves ITS OWN estimated bound (prompt bytes + framing + the output cap set on the outbound request, at the row's prices captured for that attempt), only in the supported contract (BOUNDED_ADAPTERS below, plain text chat, per-token pricing); anything else is refused before dispatch. Sends go through one governed dispatch (breaker admission, pool-selected key). MAX_ATTEMPT_MICROS caps one attempt's bound. Unsettled reservations become unknown exposure (at their bound) after the timeout and count against MAX_UNKNOWN_MICROS until reconciled; nothing is refunded automatically. An observed cost above its bound is recorded as spent and FENCES the account until an operator clears it (POST /admin/quality/auxiliary/unfence). RESERVATION_MICROS is retired: setting it refuses startup.

- `POINTSMAN_QUALITY_AUX_ENABLED` - defaults to `false`.
- `POINTSMAN_QUALITY_AUX_ACCOUNT_ID` - defaults to empty.
- `POINTSMAN_QUALITY_AUX_MAX_CALLS` - defaults to `0`.
- `POINTSMAN_QUALITY_AUX_MAX_SPEND_MICROS` - defaults to `0`.
- `POINTSMAN_QUALITY_AUX_MAX_UNKNOWN_MICROS` - defaults to `0`.
- `POINTSMAN_QUALITY_AUX_MAX_CONCURRENT` - defaults to `0`.
- `POINTSMAN_QUALITY_AUX_MAX_ATTEMPT_MICROS` - defaults to `0`.

The adapter types whose per-token billing the auxiliary bound is declared to fit. An auxiliary attempt to any other adapter - or in a form outside the supported text-chat contract - is refused before sending. The bound is an estimate under a byte-level-tokenizer assumption, not a universal limit; an overrun fences the account.

- `POINTSMAN_QUALITY_AUX_BOUNDED_ADAPTERS` - defaults to `openai,openai_compatible`.
- `POINTSMAN_QUALITY_AUX_RESERVATION_TIMEOUT_MS` - defaults to `60000`.

Optional model judge: no default provider or model; a proxy until calibrated, never a certification. PAYLOAD_CONSENT off means no content leaves the request path and the judge abstains.

- `POINTSMAN_QUALITY_JUDGE_ENABLED` - defaults to `false`.
- `POINTSMAN_QUALITY_JUDGE_PROVIDER` - defaults to empty.
- `POINTSMAN_QUALITY_JUDGE_MODEL` - defaults to empty.
- `POINTSMAN_QUALITY_JUDGE_TIMEOUT_MS` - defaults to `30000`.
- `POINTSMAN_QUALITY_JUDGE_MAX_EVIDENCE_CHARS` - defaults to `8192`.
- `POINTSMAN_QUALITY_JUDGE_MAX_CONCURRENT` - defaults to `1`.
- `POINTSMAN_QUALITY_JUDGE_PAYLOAD_CONSENT` - defaults to `false`.

Decision-only observation records what quality-guarded WOULD choose for groups on other strategies, changing nothing served. Alternative answers are a separate, paid permission: rate [0, 0.1], per instance per minute [0, 600].

- `POINTSMAN_QUALITY_OBSERVATION_DECISION_ONLY` - defaults to `false`.
- `POINTSMAN_QUALITY_OBSERVATION_ALTERNATIVE_ANSWERS` - defaults to `false`.
- `POINTSMAN_QUALITY_OBSERVATION_ALTERNATIVE_RATE` - defaults to `0.0`.
- `POINTSMAN_QUALITY_OBSERVATION_MAX_ALTERNATIVES_PER_MINUTE` - defaults to `0`.

Bounded cascade (EXPERIMENTAL): for the listed quality-guarded groups, the routed answer is held until a DETERMINISTIC FORMAT verifier accepts it (shape, not correctness: a well-formed wrong answer passes; semantic verification is NOT IMPLEMENTED); on reject/abstain ONE escalation to the profile's baseline runs on the operator account - only if this caller may use that baseline for this request - and is released only if it is accepted too. Otherwise the request is refused (502 quality_cascade_unverified); streams and tool requests to these groups are refused at the door (400 quality_cascade_unsupported). Enabling needs QUALITY_ENABLED, an enabled aux account, groups, a verifier (json-object | json-fields:<f,...> | exact-choice:<A|B|...>) and a stage ceiling on the escalation's UPPER- BOUND cost (prompt estimate + max_tokens or the row's max output, at the row's prices).

- `POINTSMAN_QUALITY_CASCADE_ENABLED` - defaults to `false`.
- `POINTSMAN_QUALITY_CASCADE_MAX_ESCALATIONS` - defaults to `1`.
- `POINTSMAN_QUALITY_CASCADE_MAX_STAGE_MICROS` - defaults to `0`.
- `POINTSMAN_QUALITY_CASCADE_GROUPS` - defaults to empty.
- `POINTSMAN_QUALITY_CASCADE_VERIFIER` - defaults to empty.
- `POINTSMAN_QUALITY_CASCADE_ESCALATION_TIMEOUT_MS` - defaults to `30000`.


---

# The learner (policy-service)

<!-- GENERATED FROM docker/learner.env.example -->

The learner reads `docker/learner.env`, separately from gateway configuration. Keep it on a private network and do not give it gateway provider keys: it has no authentication boundary of its own. Settings are typed and validated at startup; an out-of-range value refuses startup with the field named.

## LEARNING

Non-stationarity (R4): the per-observation discount on every arm's posterior. 0.999 is an effective memory of about a thousand observations; lower it to forget faster, raise toward 1.0 to remember longer. In (0, 1].

- `BANDIT_DECAY_GAMMA` - defaults to `0.999`.

The ceiling on alpha+beta per arm after discounting, so a long-lived arm cannot become immovable. Positive.

- `BANDIT_POSTERIOR_CAP` - defaults to `1000`.

Page-Hinkley change-point detection: on a detected shift the arm's posterior is reset so the learner relearns rather than averaging two regimes. On by default (since 15 Oct 2026). delta is the tolerated drift per observation, lambda the alarm threshold; smaller lambda alarms sooner and more often.

- `BANDIT_CHANGEPOINT_ENABLED` - defaults to `true`.
- `BANDIT_CHANGEPOINT_DELTA` - defaults to `0.02`.
- `BANDIT_CHANGEPOINT_LAMBDA` - defaults to `5.0`.

Warm start (R7): how many live observations an offline prior is WORTH when POST /policy/seed is called without an explicit strength. Ten is a ruled judgement (it moves the cold start, washes out, crosses over fast when wrong, and stays under the confidence gate); the reasons are in ThompsonRegistry.seed. Read when the service starts.

- `BANDIT_SEED_STRENGTH` - defaults to `10`.

Per-tenant overrides of gamma, cap and changepoint, as a JSON map. This is the one map that CAN be set from the environment (the learner parses it itself), unlike the gateway's Spring-bound maps. Invalid JSON is ignored with a warning, never a partial apply. Empty by default; the shape is {"tenant-x": {"gamma": 0.99, "cap": 500, "changepoint": true}} with the keys gamma, cap and changepoint, each optional.

- `BANDIT_TENANT_CONFIG` - defaults to empty.

DORMANT: these two govern only the retired /policy/decide path (LinUCB), which no gateway code calls. They reach the process and change nothing live. Listed so nobody tunes them expecting an effect.

- `LINUCB_ALPHA` - defaults to `0.5`.
- `BANDIT_PENDING_TTL_SECS` - defaults to `30`.

Stop new learning rather than evict retry/revision identities when its history budget is full. Existing retries remain idempotent. Since the history lifecycle (17 Oct 2026) this caps the LEGACY ts1 rows only: they carry no decision time, so they can never be fenced and are never compacted.

- `BANDIT_REWARD_EVENT_LIMIT` - defaults to `1000000`.

ts2 learning-history lifecycle (docs/operations/learning-history.md). The supported window, seconds: an event for a decision older than it is refused outside_history_window, and older ts2 history is compacted under a committed watermark (its contribution stays in the arm state). The service REFUSES to start below envelope TTL + retry horizon + skew (3000 s at the defaults below).

- `BANDIT_HISTORY_WINDOW_S` - defaults to `7200`.

Clock-skew allowance between gateway and learner, seconds (also the furthest a decision may be dated in the future).

- `BANDIT_HISTORY_SKEW_S` - defaults to `300`.

The gateway's decision-envelope validity this learner assumes: pointsman.bandit.decision-memory-ttl-min x 60. A gateway honouring longer envelopes warns and sets gateway.bandit.learner.history.window_short.

- `BANDIT_HISTORY_ENVELOPE_TTL_S` - defaults to `900`.

The longest a gateway may hold a reward before its last delivery attempt (bounded queue x retries).

- `BANDIT_HISTORY_RETRY_HORIZON_S` - defaults to `1800`.

Retained ts2 events; steady state is about arrival rate x window. When decisions still inside the window fill it, learning is refused history_full_within_window (507) until the window moves on.

- `BANDIT_HISTORY_MAX_EVENTS` - defaults to `1000000`.

Background maintenance interval, seconds, and the most event rows one compaction pass removes while holding the registry lock.

- `BANDIT_HISTORY_MAINTENANCE_S` - defaults to `60`.
- `BANDIT_HISTORY_BATCH` - defaults to `5000`.

SQLite writer-lock wait per connection, seconds; >0 and <=300. Restart learner after changing.

- `BANDIT_DB_TIMEOUT_SECS` - defaults to `5.0`.

Dormant /policy/decide pending-state cleanup interval, seconds; >0 and <=3600.

- `BANDIT_PENDING_SWEEP_INTERVAL_SECS` - defaults to `60.0`.
