jev-http

jev-protocol says what to ask and jev-client how to retry; this sends it. One POST to https://api.typesafe.ai/v1/systemone over HTTP/2 (src/transport.rs, ported from postjevsql-pg's https.rs onto tokio), and a client that is meant to be kept so the TLS connection stays warm - a cold handshake costs more than the answer does.

let model = jev_protocol::ModelId::pinned("jev-1.13.0")?;            // never an alias
let jev = jev_http::Jev::from_env("native", model).expect("TYPESAFE_API_KEY")?;   // never a literal
let mut questions = jev_protocol::Questions::new();
let raining = questions.noul("raining", jev_protocol::Noul::new(jev_protocol::Json::text("Is it raining?")))?;
let answered = jev.ask(&state, &questions, "why this question").await?;
println!("{} in {:?}", answered.response.get(raining).noul, answered.took);

"native" is which binary is asking - native, player, jevprobe, jevhooks, ... - recorded against every spend in ledger's shared ledger. See "The lifetime ledger" below for what that buys.

The lifetime ledger and its guards

The account behind this key has a few dollars of credit, ever. ledger.rs is an on-disk, flock-guarded, append-only log of every request any binary has ever made ($XDG_STATE_HOME/jev/spend.jsonl), and Jev::ask has it admit every single request before a byte is sent, whichever process is asking: Ledger::admit reads the file, checks the guards and appends a hold for the admitted request, all under one lock, so two processes asking at the same instant are serialised and the second sees the first one's question. The real cost is appended against the hold when the answer arrives (settle), or the hold is released unanswered (release). Jev::new takes a Ledger as a required argument: there is no client that skips this.

GuardDefaultWhen it refusesHow it clears
Question bucketJEV_MAX_QUESTIONS_PER_MINUTE = 3030 questions admitted in the last 60 s, by all processes togetherBy itself. Each question's token comes back 60 s after it was spent - the ledger's own recent lines are the bucket. Failure::Throttled with the seconds until the next token.
Hour breakerJEV_MAX_DOLLARS_PER_HOUR = $0.25the last hour's spend plus this request's worst case is over the limitBy itself, once enough of that spend is over an hour old. Failure::Throttled.
Out of credit-the vendor answered "no credit" (inferred shape, digest §10)Only when the account is topped up: Ledger::credit_restored. Failure::Budget.

There is no lifetime cap. An earlier version of this file refused once lifetime_usd crossed a self-imposed JEV_LIFETIME_CAP_USD ($4.00, never above an ABSOLUTE_LIFETIME_CEILING_USD of $4.50) - removed 2026-09-22, the user's own ruling on seeing it ("not this random $4 number"). A made-up ceiling on the account's own money was never this project's number to invent. What stays: the two RATE guards above (they bound how FAST money can go out, not how much, ever), and the vendor's own out-of-credit answer, which is a fact about the account rather than a guess - the only hard stop on total spend now. Status::line shows what has been spent, plainly, with no cap and no denominator.

Why the lifetime total is shown "estimated". There is no vendor endpoint to check it against (below), so it is booked from our own recorded token counts plus one guessed opening line for whatever was spent before the ledger existed. That guess was revised once, 2026-09-22, against the user's own TypeSafe dashboard reading (last 7 days, $0.1876 - the account's entire history to date): subtracting the ledger's own real spend at that same moment ($0.0359) leaves $0.1517 of real pre-ledger spend, not the $0.25 first guessed on 2026-09-20 - see ledger.rs's OPENING_ESTIMATE_USD for the arithmetic. It is still a guess, which is why it stays flagged rather than being trusted exactly: the dashboard itself is marked "*Estimated", and nine more days of play will make the pre-ledger fraction of the total smaller and smaller regardless.

Nothing a caller is refused by a throttle is retried here: the caller goes on without an answer (decisions' Engine falls back to that decision's own upstream pick) and does not ask again until the time it was told.

Why the rate guards heal themselves. Until 2026-09-21 a trip of either breaker wrote paused.json, which blocked every process until a human removed it; that day an agent tripped the question breaker (two headless runs and the window asking together) and "cleared" it by deleting the file. A guard that needs a human to delete a file will have its file deleted. Now there is no file: the bucket is the ledger, and it refills as the window slides. The hour breaker reopens by itself too, on purpose: it is a RATE guard, not a budget - it bounds how fast money can go out, never how much, ever - so a latch a human must reset would add nothing but a file somebody deletes. The only thing that stops questions for good is the vendor's own out-of-credit answer, which no amount of waiting clears either, because it is a fact about the account, not a rate.

A process killed mid-request leaves its hold unsettled; it keeps counting its worst case (a few hundredths of a cent) forever, the conservative direction.

Jev::status / Ledger::status say all of this as one value, which the window's Bot panel shows and the MCP budget tool returns.

The vendor's own documented ceilings (250,000 tokens/second, 1,200 requests/minute - jevsnes research/jev-api-digest.md §4) are respected with a per-process client-side bucket as a courtesy against a 429; the shared question bucket above is far stricter.

The ledger was opened once, at $0.1517 (revised 2026-09-22; $0.25 originally), estimated, dated to when this mechanism was built - the true spend before any of this existed, since the vendor exposes no usage endpoint to read the real figure from (§10).

No balance/usage/credit endpoint exists to read instead - checked directly, 2026-09-22, on top of the digest's own §10 (which had already grepped the vendor's full doc dump and found none):

CheckedGradeResult
GET api.typesafe.ai/v1/{usage,billing,balance,credits,credit,account,organization(s),me,whoami,spend,api-keys,keys} and a few nested variants, with the real keyAuthoritative (the API itself)Every one 404 {"detail":"Not Found"}.
GET api.typesafe.ai/v1/modelsAuthoritativeLists model aliases and release dates only (§1); no account field.
docs.typesafe.ai/apiAuthoritative (vendor docs)Documents exactly one endpoint, the evaluation POST.
docs.typesafe.ai/llms.txt (the full site index)Authoritative44 sections; none titled account, billing, usage (outside the SDK's per-response Usage type), credits, balance, organization, dashboard or console.
typesafe.ai home pageAuthoritativeLinks a "Sign in" to console.typesafe.ai - a session-authenticated web dashboard, not a documented API route the key can call. This is where the user's own dashboard reading comes from.
Web search for a GET /api/v1/credits-shaped routeAnecdotal, and almost certainly not the vendor - jev-ai.pro, jevapi.org, jevtypesafeai.com are unofficial products trading on the "Jev" name (the same kind of copycat the brain's ecosystem/ bundle already catalogues by the dozen), not api.typesafe.ai. Probed directly against the real API anyway (above): 404.

So Ledger's file is the only account of spend this project has, and Status::line's lifetime figure is shown "estimated" rather than exact for exactly this reason.

Host-only

The protocol (jev-protocol) compiles for wasm32-unknown-unknown; this crate does not, and does not need to: api.typesafe.ai refuses browser origins outright (a CORS preflight from https://example.com returns HTTP 400 "Disallowed CORS origin", measured 2026-09-20), so a web build talks to a relay that holds the key server-side. The relay speaks jev-protocol's types too; only the transport differs. That is the whole reason the protocol and the transport are separate crates.

TLS without C

rustls with the RustCrypto provider and compiled-in Mozilla roots - no OpenSSL, no ring, no aws-lc, nothing to build with a C compiler or cmake, and nothing read out of /etc/ssl. hyper drives the HTTP/2 connection directly, so a refused stream or a GOAWAY is told apart from an interrupted request (TransportErrorKind::NotSent), and resent without risking a second charge. Endpoint::trusting adds a PEM file to the roots, for jev-mock's throwaway CA and private endpoints.

The key

TYPESAFE_API_KEY, from the environment and from nowhere else. It is never logged, never printed, never written to a file, and never put on a command line; the repository README says how it gets into a process (op-env-run -- …). Jev::from_env returns None when it is absent, which is not an error - the app runs, with the loop switched off, and says why.