MIDNAT Intelligence is the layer that writes prose about the exchange: the market desk, order intelligence, and the Ask surface. This document states the law it runs under, the guardrails every piece of its prose passes through, the model routing and the exact default model id, the fair-use limits and the daily provider cost breaker, and what happens when a limit or the breaker trips.
Runnable public sources:
packages/intelligence-core/src/agent.ts,
packages/intelligence-core/src/aiGuardrails.ts,
packages/intelligence-core/src/freshness.ts,
packages/intelligence-core/src/intelligence/guidancePolicy.ts and
packages/openrouter-ai/src/client.ts.
The public agent is an import-closed reference boundary, not MIDNAT's official hosted scheduler or market runtime. It accepts an injected deterministic fact tool and an optional model gateway, validates strict JSON through the same production guardrail pipeline, falls back to deterministic copy on any provider or validation failure, and returns input/output hashes, version stamps, source labels and freshness reasons. Its tests run without a database, chain writer, provider key or network access.
The deterministic engine computes every number. The language model writes prose only. AI interprets financial truth; it never authors it. Facts, scenarios, verification and what changed are computed by the engine and never depend on anyone's model allowance. When the model cannot run, or when its output fails a guardrail, the surface falls back to deterministic copy rather than to a guess.
This is enforced, not just stated. Every piece of AI prose that reaches a
trading surface passes through one validation pipeline in
aiGuardrails.ts. It is made of pure functions with no IO, and the same
pipeline guards the desk, order intelligence and Ask, so there is exactly one
definition of acceptable output on this exchange.
The pipeline (runIntelligencePipeline) runs three validators in a fixed order:
truth first, boundary second, cosmetics last. Repaired text is re-validated
against the first two, because a repair that dropped a sentence could change
what the remaining text says, and a validator that trusts its own edits is not a
validator.
validateAuthority is the strict validator and is never relaxed to make output
flow more freely. It refuses prose that states a number or symbol the fact pack
does not support. A wrong number on a trading surface is a defect of a different
kind from a long one, so its failure sends the surface to deterministic copy.
It has three parts:
- Symbol isolation. Rejects prose that names any other configured market.
Tickers are matched case-sensitively as standalone uppercase tokens, so
lowercase words never false-positive while
TSLA,TSLA'sand(TSLA)are all caught. - Numeric consistency. Every number written in the prose must match a value the writer was entitled to state, within the precision it was written at (half a unit in the last place) or 0.5% relative tolerance, whichever is looser. Spelled numbers are rewritten to digits before checking, but only where a unit follows, so "one of the markets" stays invisible while "thirty bps" does not. A numeral written in non-ASCII digits fails closed rather than being transliterated.
- Duration authority. Durations are exempted as phrases, keyed by unit, not
as bare numbers. This keying is load-bearing. A real 5-minute age authorizes
"5 minutes"; it does not authorize "5 hours", and it does not authorize an
invented "5 bps" or "5x", because the value 5 is never added to the general
allow-list. Only integer durations are recognized, so "1.5 minutes" stays a
claim; a leading sign,
$or digit is refused, so "-5 minutes", "$30" and the "5" inside "1.5" cannot be read as durations. Both single spans ("15 minutes") and ranges ("15 to 17 minutes", "15-17 minutes") are recognized, and both bounds of a range must be authorized in that unit or both numerals face the validator. A unit is only authorized once the age actually reaches it, so a 45-second age never licenses "1 minute", and the tolerance is zero because renderer and validator round from the same stored seconds.
The quantity rule is the numeric-consistency check above: it governs how many, how much, how wide, how large. Its whole job is to stop the model from inventing or drifting a quantity. The duration rule governs how long, and is kept separate precisely so that a true duration can never license a false quantity that happens to share its digits.
validateGuidanceBoundary (backed by guidancePolicy.ts) checks whether the
prose stays inside the product's decision boundary. It is semantic, not
vocabulary-based: "risk", "long", "short", "loss", "leverage" and "exposure" are
ordinary financial words and their presence is never a violation.
The policy classifies text into six guidance levels. Levels 1 to 5 are
permitted subject to truth validation: fact, explanation, risk awareness,
tradeoff analysis, and non-directional mechanical option. Level 6,
DIRECTIONAL_RECOMMENDATION, is outside the boundary and causes a fallback. The
rule classes that push text to level 6 are:
| Rule | What it refuses |
|---|---|
DIRECTIONAL_ACTION |
Telling the reader to take a directional trade ("short TSLA", "buy") |
PERSONALIZED_INSTRUCTION |
Instructions aimed at the individual reader's own position or account |
SUITABILITY_CLAIM |
Claims that something fits the reader, is right for them, or is affordable |
CERTAINTY_CLAIM |
Presenting an outcome as certain |
UNSUPPORTED_PREDICTION |
Predicting where the price goes |
INVENTED_QUALITATIVE_LABEL |
Applying a qualitative label the fact pack did not supply |
The reason each of these refuses is the same: MIDNAT explains what is true and what the mechanical consequences are, and it does not tell a trader what to do, predict where a price goes, or judge whether a trade suits them.
Formatting is not a safety property, so it is separated from the two above.
Presentation problems are repaired, not rejected. repairField unwraps markdown
emphasis, resolves dashes by context (a dash between digits becomes "to",
elsewhere it becomes a comma), collapses newlines, drops a whole style-phrase
sentence rather than mangling grammar, and fits the text by dropping whole
sentences from the end before, as a last resort, clipping a single overlong
sentence at a word boundary with a visible ellipsis. A field is repaired only
when it exceeds a generous hard ceiling (1.5x the soft budget by default), so a
four-character overrun never costs a whole read. A fallback happens only when no
clean presentation can be produced for a required field.
The hosted Ask router works in stages:
- Deterministic short-circuits and pinned questions answer without a model where they can.
- Otherwise one small model call classifies the question into exactly one
intent (for example
EXPLAIN,POSITION,PORTFOLIO,VERIFY). Facts, scenario tables and order intent are computed deterministically. - For interpretive intents only, a second model call writes the prose fields, which then pass through the pipeline above.
The public equivalent is ModelGateway in
packages/intelligence-core/src/agent.ts. The included OpenRouter client is a
credential-lazy provider adapter. Developers must pass an explicit model id;
the package never silently spends money or selects a model when no gateway is
configured.
The separately operated hosted desk currently reads this default from its runtime:
export const DESK_MODEL = process.env["AI_DESK_MODEL"] ?? "anthropic/claude-fable-5";It is overridable by the server-side AI_DESK_MODEL environment variable.
That hosted runtime and its budget/usage orchestration are intentionally not a
turnkey part of this repository. The runnable public agent requires an explicit
model string and otherwise returns deterministic output.
Every threshold lives in one place, policy.ts, is environment driven, and is
stamped with a version so telemetry from one tuning is never silently compared
against another. The principle is "limit abuse, not curiosity": a trader
researching all day should never think about the limits, while a script meets a
wall within seconds. The values below are the starting defaults read from
source; each is overridable by its environment variable.
Work is counted in internal Intelligence Units, deliberately not called tokens: they are neither LLM tokens nor anything on a chain. A deterministic answer costs 0 units, a standard operation 1, and a portfolio synthesis 2.
| Limit | Default | Notes |
|---|---|---|
| Connected daily capacity | 300 | one generous trading day of interpretation |
| Anonymous daily capacity | 75 | a quarter of connected; no wallet required to ask |
| System daily units | 600 | the desk's own budget, never a trader's |
| Refill window | 24 h | capacity refills continuously over this window |
| Network daily multiplier | 25 | an address gets 25 connected traders' worth |
| Connected burst capacity / refill | 45 / 30 per min | stops automation, not a thinking trader |
| Anonymous burst capacity / refill | 25 / 20 per min | |
| Network soft burst | 300 / 250 per min | past it an address keeps answers, loses model passes |
| Network hard burst | 600 / 600 per min | the flood wall; the only address limit that refuses outright |
| Concurrency per actor / global | 4 / 16 | queue waits 8 s, queue depth 2 per actor |
Identity comes from the client, because MIDNAT has no accounts: a wallet and a session id are asserted by the app, not proven. That is enough to keep two real traders in one office apart. It is not enough to stop fabricated identities, which is why the network ceiling underneath it is keyed on the address the packets arrived from. A shared office or carrier NAT stays inside the network limit; a farm of fabricated wallets behind one address meets it.
Separately from any trader's allowance, the service protects its own total
spend (service.ts). This asks whether MIDNAT as a whole is spending more today
than it intends to, measured in provider-reported dollars, not estimates,
because the gateway returns usage.cost on every call.
| Setting | Default | Variable |
|---|---|---|
| Daily cost limit (USD, UTC) | 40 | AI_GLOBAL_DAILY_COST_LIMIT |
| Warning fractions | 0.6, 0.8, 0.95 | AI_GLOBAL_WARNING_THRESHOLD |
Each warning fraction raises an operator alert once. When spend reaches the limit, an error-level log records that model interpretation is paused for the rest of the UTC day. Today's spend is recovered from the event log on restart (and only real, non-synthetic rows are summed), so a restart does not hand the service a fresh daily budget.
Two different behaviours, by design.
A fair-use limit on the Ask surface degrades the answer; it does not refuse
it. The deterministic answer is still computed and returned, and the response
carries a usage disclosure telling the client that model interpretation is
limited right now, why, when it returns, and how much room is left. There is no
permanent meter and no score to optimise against.
The hard network burst limit and the concurrency limit refuse outright. The
service replies with a structured refusal carrying a reason, an HTTP status and
a Retry-After header, so there is nothing for the client to guess.
The cost breaker stops model spend, not the service. When the daily cost limit is reached, model interpretation stops for the rest of the UTC day. Deterministic Intelligence, market state, scenarios and verification keep working. A provider rate limit (HTTP 429) is tracked as a third, separate condition: it is the provider refusing, which is neither the trader's allowance nor MIDNAT's budget.
Every one of these paths ends in the same place: the reader gets a deterministic answer computed by the engine, never a fabricated one.