Privacy statement

Also: Terms · Neutrality · Copyright · Tester terms

Version 1.14, effective 2026-10-13. Adopted by Sparks & Sawdust LLC, which operates the service. MODEL-70. Version 1.0 was adopted on 2026-09-19; what changed since is listed under Changes.

This describes what the service does today, not what it is planned to do. Most claims below name the file that makes them true, so they can be checked and so they fail visibly when the code changes. Where something is built but not yet switched on, it is marked not yet live, and what is said of it describes the service only once it is switched on.

The short version

We do not ask for your prompts: no field of the rank, decide or policy-check API takes one. The feedback endpoint has an optional free-text note, and asks you not to put a prompt, a key or personal details in it. We do not proxy your model calls, so the content of your inference never reaches us. The decision API keeps nothing from the content of a request: it reads your request, computes an answer, returns it and forgets it. The one exception is the decide page's visit gate, which keeps hashed digests of each page question until the next UTC midnight, as described below. The separate release-signal automation described below stores authenticated public release notices while it processes them. Feedback you choose to send about an answer is described below; storing it is not yet switched on. If you use an API key or buy credits, we keep a hash of the key (never the key), its usage counters and credit balance, and the Stripe identifiers of your purchase. Stripe, not us, handles your card. Cloudflare, our infrastructure provider, records request metadata as platform logs.

We want to know where our visitors come from and how they use the site, so we can make a better product. That is why we run Cloudflare Web Analytics. It tells us where visitors come from, which pages they use and how fast those pages load. It does not tell us who you are: it sets no cookie, and nothing it shows us identifies you.

What a request contains

POST https://api.modelspec.dev/v1/rank accepts a profile, not a prompt:

That is the whole surface (api/worker/src/rank_service.py). There is no field for prompt text, document text, user content or an identifier of your end user, so there is nothing of that kind for us to receive, log or store. Widening these fields toward prompt text would be a breach of the commitment in the terms, not a feature release.

A rank body is capped at 16 KB; a larger one is refused, and nothing of it is kept.

POST https://api.modelspec.dev/v1/policy-check accepts a policy: the licence types, origin countries, processing regions and commercial-use requirement you want checked, an optional name for the policy, and which models, platforms and verdicts to return (api/worker/src/policy_service.py). It has no field for prompt text either. Its body is capped at 256 KB.

POST https://api.modelspec.dev/v1/decide accepts a decision spec (api/worker/src/decide_service.py; the fields are defined in docs/decision-contract.md): the capabilities you require, conditions on catalogued facets, what to optimise, how much explanation to return and how many results. A spec may also carry an estate: the providers, subscription plans and devices you hold, named by catalogue ids. POST /v1/compare takes the same spec and the id of an earlier published snapshot to compare it against. A field the spec does not define is refused, not ignored. The contract's one free-text field, task, is refused in this version: a spec that sets it is rejected, not answered (decision/contract.py). /v1/compare reads only spec and compare_to from its body and ignores anything else. Their bodies are capped at 64 KB.

POST https://api.modelspec.dev/v1/feedback accepts feedback on an answer: a rating (reliable, unreliable, trustworthy, untrustworthy or confusing), a client (agent, cli, mcp or page), and, optionally, the decision_id of the answer, a note of up to 1,000 characters, what you were trying_to_decide in up to 300 characters, the site page and the template you used (api/worker/src/feedback_service.py). No key is needed and none is read. Any other field is refused. Its body is capped at 4 KB. Before anything is kept, text that looks like an email address, a phone number, an IP address, a card number, a social security number or a credential is replaced by a placeholder, and the query of a URL is removed; this catches the shape of such text, not its meaning, so please do not send a prompt, a key or personal details in the note. The feedback endpoint (MODEL-221) is disclosed here before it is deployed; until it is, /v1/feedback answers 404 and the Feedback button described under The websites is not shown.

The remote MCP server at https://api.modelspec.dev/mcp (mcp/) is stateless. It passes each tool call through to those endpoints or to the public export, forwarding the Authorization header you sent, and stores nothing. Its feedback tool forwards no Authorization header; it passes your address instead, used only for the feedback limits described under The feedback store.

What we store

Nothing from the content of a rank, decide, compare or policy-check request. Those endpoints compute each answer from your request, return it and forget the request: no body, no field of it and no answer is written anywhere. The release-signal intake is the deliberately narrow exception described below; feedback, once its storage is switched on, is another; and the visit gate on the decide page keeps hashed digests of each page question for the day, described under The visit gate on the decide page.

The Worker binds two KV namespaces (api/worker/wrangler.jsonc) and two Durable Objects (CREDITS and HUMAN_GATE), each described below. DETERMINATIONS holds our own research — the licence and data-residency determinations the paid tier serves — and the Worker only ever reads from it. There is no code path that writes to it. No D1 database, R2 bucket, queue or analytics dataset is bound.

The API-key store

A second KV namespace, ACCESS, is bound for API keys (MODEL-69) and purchases (MODEL-73). Keys are enforced (ACCESS_ENFORCED is "true"): a rank, decide, compare, policy-check or vocabulary request that carries no key is refused with HTTP 401, and nothing is written for it, unless it comes from the decide page with a valid visit token (see The visit gate on the decide page). The health check and the feedback endpoint need no key. A request that presents a key is checked against the store. A key is issued when a purchase is claimed. It holds these kinds of record (api/worker/src/access_keys.py, access_limits.py, access_billing.py):

It holds no prompt, no request body, no field of a rank, decide, compare or policy-check request, no answer, no IP address and no user-agent: our code reads none of those into it. A rank, decide, compare or policy-check request that presents no key, or a test_ sandbox key, writes nothing to it. Card data never reaches this store: Checkout is hosted on Stripe.

The release-signal queue

The same ACCESS namespace can hold an authenticated release signal from Grok Bot (MODEL-113). This automation ships off behind SIGNALS_ENABLED; while that flag is false, the endpoint returns 404 and writes nothing. When Jamie enables it, each pending record contains exactly the public model name, provider name, first-seen X URL, timestamp, confidence and signal id submitted by the bot. X is discovery only: none of these values becomes model-card evidence.

A second submitter, our primary-source release watcher (MODEL-216, release_signals/watch.py), files the same fields for a release it finds on a lab's or provider's public model page, a provider's public model list or a lab's Hugging Face feed, with that page's URL as the first-seen URL. It authenticates with the repository workflow's read key. For each of its discoveries the queue also keeps a marker holding the signal id and timestamp, so the same discovery is not queued twice; the marker is kept permanently. Its discoveries are not model-card evidence either.

After the repository workflow handles the signal, it stores an audit record with that signal, the processing date, result and pull-request or issue URL. It also schedules copies for re-checks after 1, 7 and 30 days. A pending or scheduled record is deleted when acknowledged; the audit record remains so the automation's actions can be reconstructed. The queue stores no prompt, completion, private X message, API key, IP address or user-agent. SIGNALS_ENABLED must be on before intake accepts anything. Grok Bot's intake also needs the HMAC write secret, and the watcher's needs the read key, which also protects retrieval and acknowledgement by the repository workflow (api/worker/src/signals_service.py).

The feedback store

A third KV namespace, FEEDBACK, is to hold feedback on answers (MODEL-221). It is not yet live: FEEDBACK_ENABLED is "false" and the namespace is not bound, so feedback is checked, answered and not kept. While storage is off, the only limit applied is a per-minute count kept in the running Worker's memory, under a keyed hash of your address, and nothing is written anywhere.

When it is live, each piece of feedback is one record holding exactly: the day it was received (received_on, never the time of day, which the record's expiry does not reveal either), the rating, the client, and whichever of decision_id, page, template, note and trying_to_decide you sent, after the replacement described above, plus which kinds of text were replaced (redacted). It holds no IP address, no key, no user-agent, no origin and no time of day. Records are deleted automatically between 180 and 181 days after they are received. A decision_id identifies the question that was asked (it is derived from the question and the data it was answered from), not who asked it; anyone who can guess the whole question can recompute it.

A recorded response gives you a random receipt; we keep only its SHA-256 hash. Sending the receipt to DELETE /v1/feedback deletes the stored record at once. It cannot reach a copy already made for the weekly review described below.

To limit abuse without keeping addresses, the Worker will then also count feedback per address (for IPv6, per /64 network) per day under a name derived from the address with a keyed hash (HMAC-SHA256 with a secret that is not published) and the day, so the count cannot be turned back into the address or linked across days, and it will keep a per-day total for the website and one for the API, which name no address. These counts expire within two days. The ModelSpec MCP server passes its caller's address to the Worker for these limits only.

Once storage is live, once a week we will copy the feedback to our own computer, count it by rating, page and template, and group repeated negative feedback into issues in our private issue tracker (Linear), with the decision IDs and short excerpts of the scrubbed note and trying_to_decide text. The issues are drafted and filed with the help of an AI assistant, whose provider processes that text under its terms as our processor. The weekly copies are deleted within 14 days; an issue keeps its excerpts until the issue is deleted. What we change because of feedback is listed, in our own words and never quoting anyone, at https://modelspec.dev/feedback/.

The credit ledger

A Durable Object class CreditsObject, bound as CREDITS (api/worker/wrangler.jsonc), holds credit balances (MODEL-75, MODEL-93). Credits are added only by a paid Stripe purchase (a new purchase can start only while BILLING_ENABLED is on; renewals and claims of purchases already paid are credited while it is off), or by an x402 payment in USDC on Base, made with an API key (X402_ENABLED is on). It holds these kinds of record (api/worker/src/credits.py):

These are counts, times, Stripe ids and our own identifiers. They hold no card detail and nothing from a request.

It holds no prompt, no request body, no field of a request, no ranking or policy answer, no IP address and no user-agent: our code reads none of those into it. A rank, decide, compare or policy-check request that presents no key, or a test_ sandbox key, writes nothing to it. A request that presents a live key reserves its cost against that key's balance, which can create an empty balance record under the key's hash even when nothing has been bought (api/worker/src/x402.py).

Workers KV is not this ledger. KV is eventually consistent and has no compare-and-set, so it cannot keep a balance non-negative when two requests race. The Durable Object is the serial mailbox that can.

Apart from the per-minute feedback count described above, the only other things held between requests are short-lived copies of our own catalogue, decision snapshot and determinations, which contain nothing of yours (api/worker/src/entry.py).

Our own code writes no log line about your request. There is no analytics call, no telemetry beacon and no third-party tag on the API path. Cloudflare's analytics script (see The websites) is inserted into the one HTML page the API serves, the page that shows a purchased key, but that page's content security policy stops a browser from loading it (api/worker/src/billing_page.py).

The visit gate on the decide page

VISIT_GATE_ENABLED is on in production. It lets people use the decide page without a key while separating them from automated access.

Verification. On the first request of a visit, the page runs a Cloudflare Turnstile check that is shown only if interaction is required; most people pass in the background, and when interaction is needed the challenge appears beside the answer. Your browser loads a Cloudflare script and challenge frame and connects to challenges.cloudflare.com. Cloudflare processes browser and network signals, including your IP address, TLS fingerprint, User-Agent, the site key and the origin. Cloudflare acts as our processor for protecting the site and as a controller for improving Turnstile's bot detection; see its Turnstile Privacy Addendum. We receive a challenge token and send it, with our secret, to Cloudflare's Siteverify service, checking that it succeeded and matches our hostname and the decide action. We omit the optional remoteip parameter and do not enable Turnstile pre-clearance.

The visit token. After successful verification, the Worker issues a visit token signed with HMAC-SHA256 under a separate secret, VISIT_TOKEN_HMAC_KEY (api/worker/src/visit_token.py). It binds the daily visitor id, exact page origin, the time of the original verification, issued-at and expiry. The daily visitor id is HMAC-SHA256(VISITOR_HMAC_KEY, IP | UTC day), derived from CF-Connecting-IP with an IPv6 address first reduced to its /64 network; no raw IP and no bare hash of one is stored. A token cannot be used from another visitor id or origin. The page keeps it only in memory and sets no cookie for it (web/src/decide/adapter/visit.ts). The token has a 30-minute sliding window: each admitted decide or vocabulary request renews it for 30 minutes, but never beyond four hours from the original verification. After an idle expiry, or at four hours, the page runs another check and retries once. UTC-day identity rotation invalidates the previous day's token. ModelSpec application code neither logs nor persists the Turnstile or visit token. The visit token travels in request and response headers, which Cloudflare's Workers observability may record in its invocation logs, described under What Cloudflare records.

What the gate stores. A Cloudflare Durable Object (HUMAN_GATE) keeps, for each daily visitor id, a visit meter: the count of questions admitted that day, the day, the times of recent admissions and, if triggered, a suspicion expiry. It admits at most 300 questions per UTC day and 30 in a rolling minute, and five admissions at nearly equal intervals are refused for ten minutes as automated-looking. A separate vocabulary meter keeps its day, count and recent request times and permits 60 vocabulary lookups per UTC day and 10 in a rolling minute. These numbers are operator configuration and may be changed only alongside this disclosure. For each admitted page action the object also keeps the random intent id the page sends in x-modelspec-intent, the admission's first-request time, request count and recent request times, and SHA-256 digests of the question computed from the canonical decision spec: its fixed fields, individual conditions, objective, capabilities and estate, with flags for whether the estate is absent, permits a comparison, or the question is a plot. These recognise follow-up requests for the same question and are not used for another purpose. These hashes are not encryption: someone who can guess the question can compute matching digests. No raw spec, token or raw IP is stored (api/worker/src/human_gate.py, human_gate_do.py, human_question.py). One facet action is one question, including its permitted presentation requests; an admitted question consumes allowance even if its answer fails, and verification and token renewal do not reset the allowances. After 60 seconds an intent can no longer be continued; it is dropped from storage on the visitor's next question, and in any case with the day's state. All of it is scheduled for deletion at the following UTC midnight; a delayed or retried alarm can delay the physical deletion, and Cloudflare's point-in-time recovery for SQLite-backed storage can retain earlier states for up to 30 days. Shared IPs or IPv6 /64 networks share allowances. API keys take precedence over visit tokens. Because keys are enforced (ACCESS_ENFORCED), a request with neither a key nor a valid visit token is refused and writes nothing to these meters.

What Stripe holds

Purchases are made on Checkout pages hosted by Stripe (api/worker/src/billing_stripe.py), which processes payments for Sparks & Sawdust LLC. Stripe collects your card details and the contact and billing details its Checkout form asks for, and holds them under its own privacy policy. We never receive your card number, expiry or CVC. From Stripe we keep only the identifiers listed under The API-key store and The credit ledger: event, customer, subscription, Checkout session, invoice, PaymentIntent, chargeback and Price ids. We do not copy your name, email address or billing address into our stores; they remain in our Stripe account, where we can see them to handle a request from you.

What we send Stripe for Scale overage. When a Scale plan draws metered overage, the Worker sends Stripe a Billing Meter event for each settled charge, so that Stripe can bill the overage on the plan's invoice (api/worker/src/billing_stripe.py). The event carries the meter's name, your Stripe customer id, the number of overage credits, the time they were used, and an identifier made from a random meter id that we keep with your balance record (it is not derived from your key) and a counter, which lets Stripe ignore a repeat of the same event. It carries nothing from your request. No meter event is sent while the overage Price is not for sale.

What x402 payments involve

When your software pays by x402 (api/worker/src/x402.py), it sends us a signed authorization for a USDC transfer on the Base network. It holds your wallet's public address, our receiving address, the amount, a validity window, a nonce and the signature. We do not store it. We pass it to Coinbase's x402 facilitator (api.cdp.coinbase.com), which verifies and settles it, and which handles it under Coinbase's own terms and privacy policy. The facilitator returns a transaction hash and the paying address. We return both to you in the PAYMENT-RESPONSE header and keep only what The credit ledger describes: the payment id, the transaction hash, the key's hash and the credits bought. We do not keep the paying address.

A settled transfer is recorded on the Base blockchain, which anyone can read and which no one can erase. Your wallet's public address, our receiving address, the amount and the time are public there, and anyone holding the transaction hash can find them. Choose the wallet you pay from with that in mind.

X402_PAY_TO, the address that receives x402 payments, is 0x69429dbEEEE7218084AB5B896788f5fCE452419C on the Base network. It is held in a Coinbase account for Sparks & Sawdust LLC. No private key for it is in this repository.

Keys are enforced (ACCESS_ENFORCED), so a request without a key is refused with 401 before any payment is offered. Nothing is paid or recorded without a key. A separate staging copy of the API, on a workers.dev address that the site never calls, runs x402 on a test network for testing.

What Cloudflare records

The API and the website run on Cloudflare, and Cloudflare records request metadata as any host does: the source IP address, timestamp, request method and path, response status, and user-agent. Cloudflare also asks your browser, in the NEL and Report-To headers of its responses, to report to a.nel.cloudflare.com any request to our site or API that fails to connect or is answered with an error status; a report carries the address requested, the referring page, the status and timings. It asks for no report of a request that succeeds. Cloudflare Workers observability is enabled on the API Worker and the MCP Worker (api/worker/wrangler.jsonc, mcp/wrangler.jsonc), which retains invocation logs — request metadata, outcome and any uncaught error — under Cloudflare's own retention. We use this to tell whether the service is working.

We do not export it, join it to anything else, or use it to build a profile of you. Cloudflare processes it under its own terms as our infrastructure provider.

The websites

modelspec.dev is a static site on Cloudflare Pages. benchgraph.dev redirects to it.

Cloudflare Web Analytics

We want to know where our visitors come from and how they use the site, so we can make a better product. That is why we run Cloudflare Web Analytics. It tells us where visitors come from, which pages they use and how fast those pages load. It does not tell us who you are.

Cloudflare inserts its analytics script into each page of the site as it serves the page; the script is not in our code. Your browser loads it from static.cloudflareinsights.com, a Cloudflare host, and it sends to modelspec.dev/cdn-cgi/rum, which Cloudflare answers: the page's address without its query string or the part after the #; the page you came from, shortened the same way; a random identifier made afresh for that one page load; your browser's make and version and your operating system's version; and how quickly the page loaded and responded, naming the page element involved in the slowest paint, layout shift or interaction. The script sets no cookie, uses no other storage in your browser and sends no identifier that outlasts the page load, so nothing it sends follows you from visit to visit or from site to site. Cloudflare also receives the request metadata described under What Cloudflare records, as for any request.

From this, Cloudflare shows us counts of page views and visits and page-load times, by page, referrer, country, browser, operating system and device type. Web Analytics never shows us an IP address, and nothing it shows us identifies you or any customer. We do not export it, join it to anything else, or use it to build a profile of you. Cloudflare processes this data for us, as our processor, under its own terms. Cloudflare's own description of Web Analytics and its privacy is at cloudflare.com/web-analytics.

Inference, and why there is nothing to say about it

We never sit between you and a model. We return a recommendation and hand off; your calls go to the provider, gateway or local runtime directly. We do not see your prompts, your completions, your token counts or your model traffic, and we do not meter, resell or bill any of it. This is an architectural boundary rather than a retention promise: there is no path by which that data could reach us.

The ModelSpec CLI (modelspec, Python package modelspec-dev, version 0.3.0 and later) runs on your machine and never uses, sends or logs your provider API keys. They stay with you. It connects only to api.modelspec.dev, only when you run a command that needs the service, and it sends no telemetry.

It keeps files on your machine only when you ask. If you save your ModelSpec API key with modelspec auth set, it is kept in a file under your user configuration directory: on macOS and Linux only your user can read it, and on Windows it has your profile folder's permissions. If you let modelspec setup mcp --write change an AI client's configuration, it first saves a backup of that file beside it; that file may hold other keys, which the CLI copies unchanged and never sends anywhere. We never receive either file. Versions up to 0.2.0, now withdrawn, could also keep an opt-in local log of outcomes; the current CLI has none.

Not yet live

Named so that this statement can be checked against the code, and so that nothing below is read as describing the service today:

Your requests about your data

Without a key, the service holds nothing that names you. The decide page's visit gate keeps a day's counts and question digests under a keyed daily id derived from your IP address; it is deleted at the following UTC midnight, and we cannot find it without your IP address and the day. So there is generally nothing to access, correct, export or delete. With a purchased key, we hold the records described above, linked to your Stripe customer id. To ask what we hold about you, or to have it corrected or deleted, write to [email protected]. Never send us your API key.

Feedback is not linked to you. To delete a piece of feedback, send its receipt to DELETE /v1/feedback, or write to us with it.

Changes

A change to what the service records is a change to this statement, and it is published here before the change ships. The version above is the one in force.

Feedback