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.
| Guard | Default | When it refuses | How it clears |
|---|---|---|---|
| Question bucket | JEV_MAX_QUESTIONS_PER_MINUTE = 30 | 30 questions admitted in the last 60 s, by all processes together | By 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 breaker | JEV_MAX_DOLLARS_PER_HOUR = $0.25 | the last hour's spend plus this request's worst case is over the limit | By 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):
| Checked | Grade | Result |
|---|---|---|
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 key | Authoritative (the API itself) | Every one 404 {"detail":"Not Found"}. |
GET api.typesafe.ai/v1/models | Authoritative | Lists model aliases and release dates only (§1); no account field. |
docs.typesafe.ai/api | Authoritative (vendor docs) | Documents exactly one endpoint, the evaluation POST. |
docs.typesafe.ai/llms.txt (the full site index) | Authoritative | 44 sections; none titled account, billing, usage (outside the SDK's per-response Usage type), credits, balance, organization, dashboard or console. |
typesafe.ai home page | Authoritative | Links 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 route | Anecdotal, 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.