1# jev-http 2 3`jev-protocol` says what to ask and `jev-client` how to retry; this sends it. 4One `POST` to `https://api.typesafe.ai/v1/systemone` over HTTP/2 5(`src/transport.rs`, ported from postjevsql-pg's `https.rs` onto tokio), and a 6client that is meant to be kept so the TLS connection stays warm - a cold 7handshake costs more than the answer does. 8 9```rust 10let model = jev_protocol::ModelId::pinned("jev-1.13.0")?; // never an alias 11let jev = jev_http::Jev::from_env("native", model).expect("TYPESAFE_API_KEY")?; // never a literal 12let mut questions = jev_protocol::Questions::new(); 13let raining = questions.noul("raining", jev_protocol::Noul::new(jev_protocol::Json::text("Is it raining?")))?; 14let answered = jev.ask(&state, &questions, "why this question").await?; 15println!("{} in {:?}", answered.response.get(raining).noul, answered.took); 16``` 17 18`"native"` is which binary is asking - `native`, `player`, `jevprobe`, 19`jevhooks`, ... - recorded against every spend in `ledger`'s shared ledger. 20See "The lifetime ledger" below for what that buys. 21 22## The lifetime ledger and its guards 23 24The account behind this key has a few dollars of credit, ever. `ledger.rs` is 25an on-disk, `flock`-guarded, append-only log of every request any binary has 26ever made (`$XDG_STATE_HOME/jev/spend.jsonl`), and `Jev::ask` has it admit 27every single request before a byte is sent, whichever process is asking: 28`Ledger::admit` reads the file, checks the guards and appends a **hold** for 29the admitted request, all under one lock, so two processes asking at the same 30instant are serialised and the second sees the first one's question. The real 31cost is appended against the hold when the answer arrives (`settle`), or the 32hold is released unanswered (`release`). `Jev::new` takes a `Ledger` as a 33required argument: there is no client that skips this. 34 35| Guard | Default | When it refuses | How it clears | 36| --- | --- | --- | --- | 37| 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. | 38| 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`. | 39| Out of credit | - | the vendor answered "no credit" (inferred shape, digest §10) | Only when the account is topped up: `Ledger::credit_restored`. `Failure::Budget`. | 40 41**There is no lifetime cap.** An earlier version of this file refused once 42`lifetime_usd` crossed a self-imposed `JEV_LIFETIME_CAP_USD` (`$4.00`, never 43above an `ABSOLUTE_LIFETIME_CEILING_USD` of `$4.50`) - removed 2026-09-22, the 44user's own ruling on seeing it ("not this random $4 number"). A 45made-up ceiling on the account's own money was never this project's number to 46invent. What stays: the two RATE guards above (they bound how FAST money can 47go out, not how much, ever), and the vendor's own out-of-credit answer, which 48is a fact about the account rather than a guess - the only hard stop on total 49spend now. `Status::line` shows what has been spent, plainly, with no cap and 50no denominator. 51 52**Why the lifetime total is shown "estimated".** There is no vendor endpoint 53to check it against (below), so it is booked from our own recorded token 54counts plus one guessed opening line for whatever was spent before the ledger 55existed. That guess was revised once, 2026-09-22, against the user's own 56TypeSafe dashboard reading (last 7 days, $0.1876 - the account's entire 57history to date): subtracting the ledger's own real spend at that same moment 58($0.0359) leaves $0.1517 of real pre-ledger spend, not the $0.25 first 59guessed on 2026-09-20 - see `ledger.rs`'s `OPENING_ESTIMATE_USD` for the 60arithmetic. It is still a guess, which is why it stays flagged rather than 61being trusted exactly: the dashboard itself is marked "*Estimated", and nine 62more days of play will make the pre-ledger fraction of the total smaller and 63smaller regardless. 64 65Nothing a caller is refused by a throttle is retried here: the caller goes on 66without an answer (`decisions`' `Engine` falls back to that decision's own 67upstream pick) and does not ask again until the time it was told. 68 69**Why the rate guards heal themselves.** Until 2026-09-21 a trip of either 70breaker wrote `paused.json`, which blocked every process until a human 71removed it; that day an agent tripped the question breaker (two headless runs 72and the window asking together) and "cleared" it by deleting the file. A 73guard that needs a human to delete a file will have its file deleted. Now 74there is no file: the bucket is the ledger, and it refills as the window 75slides. **The hour breaker reopens by itself too, on purpose**: it is a RATE 76guard, not a budget - it bounds how fast money can go out, never how much, 77ever - so a latch a human must reset would add nothing but a file somebody 78deletes. The only thing that stops questions for good is the vendor's own 79out-of-credit answer, which no amount of waiting clears either, because it is 80a fact about the account, not a rate. 81 82A process killed mid-request leaves its hold unsettled; it keeps counting its 83worst case (a few hundredths of a cent) forever, the conservative direction. 84 85`Jev::status` / `Ledger::status` say all of this as one value, which the 86window's Bot panel shows and the MCP `budget` tool returns. 87 88**The vendor's own documented ceilings** (250,000 tokens/second, 1,200 89requests/minute - jevsnes `research/jev-api-digest.md` §4) are respected with a 90per-process client-side bucket as a courtesy against a `429`; the shared 91question bucket above is far stricter. 92 93The ledger was opened once, at $0.1517 (revised 2026-09-22; $0.25 originally), 94estimated, dated to when this mechanism was built - the true spend before any 95of this existed, since the vendor exposes no usage endpoint to read the real 96figure from (§10). 97 98**No balance/usage/credit endpoint exists to read instead - checked directly, 992026-09-22**, on top of the digest's own §10 (which had already grepped the 100vendor's full doc dump and found none): 101 102| Checked | Grade | Result | 103| --- | --- | --- | 104| `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"}`. | 105| `GET api.typesafe.ai/v1/models` | Authoritative | Lists model aliases and release dates only (§1); no account field. | 106| `docs.typesafe.ai/api` | Authoritative (vendor docs) | Documents exactly one endpoint, the evaluation POST. | 107| `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. | 108| `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. | 109| 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`. | 110 111So `Ledger`'s file is the only account of spend this project has, and 112`Status::line`'s lifetime figure is shown "estimated" rather than exact for 113exactly this reason. 114 115## Host-only 116 117The protocol (`jev-protocol`) compiles for `wasm32-unknown-unknown`; this 118crate does not, and does not need to: `api.typesafe.ai` refuses browser 119origins outright (a CORS preflight from `https://example.com` returns HTTP 400 120"Disallowed CORS origin", measured 2026-09-20), so a web build talks to a 121relay that holds the key server-side. The relay speaks `jev-protocol`'s types 122too; only the transport differs. That is the whole reason the protocol and 123the transport are separate crates. 124 125## TLS without C 126 127rustls with the **RustCrypto** provider and **compiled-in** Mozilla roots - no 128OpenSSL, no `ring`, no `aws-lc`, nothing to build with a C compiler or cmake, 129and nothing read out of `/etc/ssl`. hyper drives the HTTP/2 connection 130directly, so a refused stream or a GOAWAY is told apart from an interrupted 131request (`TransportErrorKind::NotSent`), and resent without risking a second 132charge. `Endpoint::trusting` adds a PEM file to the roots, for jev-mock's 133throwaway CA and private endpoints. 134 135## The key 136 137`TYPESAFE_API_KEY`, from the environment and from nowhere else. It is never 138logged, never printed, never written to a file, and never put on a command 139line; the repository README says how it gets into a process 140(`op-env-run -- …`). `Jev::from_env` returns `None` when it is absent, which 141is not an error - the app runs, with the loop switched off, and says why.