Data & privacy
Sending a prompt to a marketplace means trusting two parties: the platform that routes the request and the provider that serves it. This page says exactly what each side of that trade consists of, and which controls make the trade explicit.
Where a request goes
Heimdallm authenticates, routes and forwards your request; the serving provider receives the
request body — messages, tools, parameters — and generates against it. The optional user
field, when you send one, is forwarded to the provider with the rest. A request you send is
therefore a request to one independent operator, chosen by routing you control: pin it, allow it,
exclude it, or let the default balance choose among the endpoints you left eligible.
The response and the billing record come back through Heimdallm: the model, the provider that served you, token counts, timings, the finish reason, and the exact cost.
Afterwards the provider is shown what their own endpoints did, and nothing about who asked. Their
dashboard carries every attempt against their endpoints with the model, the status their engine
answered, the timings, the token counts and the request id — and none of the account, workspace, API
key or app name the same record holds on this side. Beyond the body they were sent to generate
against, a provider learns the shape of their traffic and never whose it was.
What providers declare — and how to route on it
Each endpoint carries privacy-relevant declarations, made by its provider:
| Declaration | Field | Meaning |
|---|---|---|
| Training on prompts | data_policy.training | Whether prompts may be used to train models. |
| Retention | data_policy.retention_days | How long the provider says it keeps request data. |
| Zero data retention | compliance.zdr | The endpoint declares ZDR compliance. |
| HIPAA | compliance.hipaa | The endpoint declares HIPAA compliance. |
Declarations are per endpoint: the same weights served elsewhere can carry a different policy, and an endpoint whose provider declared nothing shows nothing — absence is never read as a promise.
A ZDR declaration is per endpoint, but the marketplace reviews the provider behind it: an operator
inspects the deployment once and approves or denies the whole provider, and that verdict covers every
endpoint the provider declares ZDR on, now and later. What the API publishes in compliance.zdr is
the verdict, not the claim: true once the provider is approved, null while it is still under
review, false when review denied it (or the provider declared false). Wherever a claim stands,
compliance.zdr_status names the verdict — pending, approved or denied — and model pages show a
claim on an unreviewed provider as ZDR pending rather than as ZDR. Providers also link a privacy policy, terms and a status page to their public profile
(GET /providers; the providers directory shows the same links). Read the policy
of any provider you intend to pin.
Two routing preferences turn declarations into guarantees:
{
"provider": {
"data_collection": "deny",
"zdr": true
}
}
data_collection: "deny"excludes endpoints that may train on prompts — and endpoints whose policy is not declared at all.zdr: truekeeps only endpoints that declare zero data retention and whose provider the marketplace has reviewed and approved — same rule, one step stronger: undeclared does not qualify, and neither does a declaration whose provider is still pending review or one that review denied.
Both can be set as workspace defaults so every key in the workspace inherits them. If your compliance story requires them, set them there rather than per request — the defaults fill any field a request leaves out.
What Heimdallm records
The record of a generation is a billing record: ids, model, provider, token counts, timings,
finish reason, cost — the numbers needed to settle and audit a request. It is queryable as the
documented shape at GET /generation, and the
ledger keeps one usage row referencing it.
The whole of it is readable in the account area rather than only downloadable: the Activity tab lists every request with the filters to find one, and a request opens onto everything recorded about it, the endpoints routing tried included. Those attempt records are kept for seven days; the generation itself is kept for as long as the account is.
Money movements carry their own references: deposits reference their on-chain transaction; refunds carry a written reason and name the generation they answer where they answer one; adjustments are a correction to the balance itself. Stablecoin transfers are ordinary blockchain transactions — the sending address, amount and receiving address are public on that chain by nature. Wallet binding is a single free signature that matches future deposits to your account; it moves no funds and grants no spending permission.
Opening an invite link sets one cookie, __Host-heimdall_ref.
It holds the referrer's eight-character code and nothing about you. It is kept for three days from
the last invite link opened, each opened link replacing the one before, and is marked HttpOnly, so
no script running in the browser can read it. The server reads it once, while rendering the sign-in
page, and it takes effect only where that sign-in creates an account.
The chat playground keeps conversations in your browser's own storage only — the server holds none of them. Its agent's two web tools are the exception to running in the tab: a URL it reads and a query it searches pass through this site, which holds neither and records neither, and carry nothing else of the conversation. A page is fetched with no cookie, no credential and no header naming the marketplace, so the site read learns nothing about you from the request.
The machine a coding workspace runs on keeps its files the same way. That disk is destroyed with the
machine, so the one durable copy of the sources under /workspace is in your own browser, in this
site's IndexedDB storage, and nowhere else. While a tab is connected, the machine sends that tab each
batch of changed files 250 ms after the last change, and no later than 2 s after the first. This site
carries those frames between your browser and your machine and keeps none of them: no file, no
filename and no hash is written to its database or its logs. A closed tab is not syncing, because a
browser that is not open cannot be written to. A change made while no tab is connected reaches your
browser when one connects again, as long as the machine is still running; what a closing tab can lose
is the batch that had not been sent yet. A fresh machine is filled from the workspace you started it
with before it handles your first message, and a machine that already holds files is the truth that
your browser catches up from, never the other way round.
Your browser holds as many workspaces as you make, and each machine holds one of them. You pick which one on the start sheet, before the machine starts, and the coding playground's home lists the machines you have running by the workspace each is on. A workspace already on a running machine cannot be given to a second one, because two machines copying into one workspace would overwrite each other's work; shutting the first machine down releases it. Naming a workspace is a rename on its card, and the name is kept beside the copy in your own browser: it is never sent, and this site has nowhere to put it. A workspace no machine holds can be deleted from its card, after a confirmation that names what goes; it takes the browser's only copy with it, and this site holds none to restore it from.
What is copied is every file under /workspace except paths nothing overrides — .git/ at any depth,
lost+found/ at the root, and .heimdall/ at the root apart from .heimdall/state/, which carries
your conversations so a fresh machine opens the chat history the previous one held — and whatever
/workspace/.syncignore names, in .gitignore syntax. Everything else is that file's to decide, and it is yours to edit: a workspace
whose root has no .syncignore is given one at boot listing the generated trees a project usually has,
node_modules/ among them, and it is an ordinary file in your workspace — open it to see the list,
remove a line to make that tree travel from then on, and add one for a generated tree of your own. The exclusions are about size and noise, not secrecy: a .env in
the workspace is copied like anything else, and your .gitignore is never consulted, because projects
routinely ignore the files whose loss would hurt most. A file over 32 MiB is skipped and counted rather
than silently dropped, and if your browser refuses the space for a batch, the copy pauses and says so
while the machine keeps working. The copy is subject to your
browser's own rules for site data: Safari deletes it after seven days of use in which you do not
interact with this site, and a browser under storage pressure can evict it unless this site has been
granted persistent storage. The coding playground asks your browser for that grant when you start a machine,
before the machine is created, and its home states whether it was given: Chrome and Safari decide on
their own from how often this site is used, and Firefox asks you to choose.
Export saves a workspace as a zip file. Your browser builds it out of what it holds and hands it
to you to save, so no part of it is uploaded and this site never sees it. Import reads such a zip
back, on this computer or another one, and always makes a new workspace rather than merging into one
you already have: reading the same file twice leaves you with two and takes nothing away from either.
What comes in is held to the same paths above and the same 32 MiB per file, and a zip entry naming
somewhere outside the workspace is refused outright, since that entry is what a machine would later be
filled from. A .syncignore is a file in the workspace and rides in the zip like any other, so a
workspace that had a line taken out of its own keeps what that line used to exclude. Whatever is left
out is named on the page rather than dropped quietly. Export and import are how a workspace moves to another browser, and the
only way it can, because nothing about it sits on a server to sign in to. A browser that has never
opened a workspace can read one in: the import makes the storage it needs, so nothing has to be
started first.
The coding playground's sandbox is an ordinary Linux machine that runs whatever you install, so an automated infrastructure-protection system watches each machine's live behaviour to keep one tenant's VM from attacking the platform or another tenant. What it observes is behaviour, not the substance of your work: which programs run and the arguments they run with, where the machine connects, and whether it opens files in sensitive categories such as ssh keys or cloud credentials — never the contents of a file it opens, the output a command produced, or your conversation with the coding agent. The command lines and destinations are what tell a package install from an exfiltration. That live view reaches a single judgement and is then discarded. Nothing about it is written down unless the session is flagged, whether as a judged escape attempt or as a window the system could not judge and set aside for review, and a flag holds only a content-free record — counts and categories, never a path, an address, an argument, or a byte of your work. The judgement that reads the behaviour runs on a provider vetted for zero data retention and is paid for by the platform, which is why it never appears in your own generation history.
One more actor deserves mention: the platform's own probes are ordinary paid requests, routed and billed like anyone's traffic, and their usage counts in the public activity figures. There is no separate, privileged path by which requests flow.
Stored files are temporary
A file uploaded to the Files API is a handoff, not storage. It expires 48 hours
after it is stored, and the moment it does every request for it is answered 404, the same as for
a file you deleted; it stops counting against your file quota, and it is left out of the archive
below. The bytes are then removed from the live database by a background purge that runs every thirty
seconds on every gateway, and whose backlog the marketplace monitors.
A request that names a stored file has the file read at the start of that request, and carries its bytes to the provider inside it, as an inline document would. The file's id is never sent and nothing is uploaded to a provider, so the only stored copy is the one in your file store, which the same 48 hours bound.
Expiry bounds what the live database holds, and the database is copied: dumps, backups and the
write-ahead log archive are kept so the service can be restored after a disaster, for far longer than
a file lives. So a file's bytes are stored encrypted, under a key that is kept outside the database and
every copy of it, and the key is destroyed no more than 50 hours after the file was stored: the
hour of uploads that share it, the 48 hours a file lives, and a one-hour margin. A copy made before a
file expired holds only ciphertext of it, and after those 50 hours nothing anywhere can open that
ciphertext. If the keys are lost sooner, the files sealed under them read as 404, the answer an
expired file gets.
Batches hold requests only while they run
A batch keeps the requests it was given and the replies it has collected only
until it ends. In the step that gives the batch its final status they are written to a results file in
your file store and cleared from the batch, and that file expires 48 hours after it is written, like
any other stored file. What remains of a batch is a record of what ran: its status and counts, the
custom_id of each request and the id it was billed under. Each request is billed as the same
request would be alone, so the usage and the ledger are the same records as for any other request.
While a batch runs, its requests and replies are stored encrypted the way a stored file is: under a key
that is kept outside the database and every copy of it, and destroyed no more than 50 hours after the
value was written, which is longer than the 24 hours a batch can run. A copy of the database made while a
batch runs holds only ciphertext of them, and nothing can open it after that. If a key is lost sooner, a
request whose turn has not come is not sent and is not charged, and a request that was charged and
answered is reported as failed with its charge named, never run a second time.
Generated video and meshes
An asynchronous video or mesh job keeps its result on the marketplace until you fetch it: a clip or mesh is deleted 10 minutes after its first delivery, and one nobody fetched when the job's 24-hour window closes. The reference images sent with such a job are held with the job while it runs and removed from it when it ends. These are held unencrypted in the live database for that long, and they are not covered by the encryption of stored files and batches above: a copy of the database made while one is held keeps it until the copy ages out under its own retention, which is 30 days for database dumps and for the write-ahead log archive. Fetch a result and it is gone from the live store within minutes; it is gone from those copies after their retention, and not before.
Downloading everything held about you
The account page's Privacy tab offers the complete record as a zip archive, generated on demand from the live database. It holds the account and its workspaces, browser sessions, API keys, wallet bindings, deposits across every rail, previews, sandbox sessions, the full credit ledger and every billed request, and every batch with its requests. Stored files that have not expired are in it twice over: their metadata as a list, and the content of each one as an entry of its own. Money figures are exact decimal strings; the unbounded tables (the ledger, generations) are JSON Lines, one row per line.
The archive and the account area answer the same question in two ways: the tabs are where a record is read, and the archive is where it is taken away.
Two scoping rules apply. Records an organization or a provider owns — their ledger, their usage, their payout details — belong to those entities and appear only as your membership facts. Records kept for platform security are not included.
Deleting your account
Deletion is requested from the same tab and runs on a 48-hour countdown. Until the countdown ends the account keeps working — signing in, keys, requests — and the deletion can be cancelled from the same place. When it ends:
- Every browser session stops, the personal account's API keys are disabled, live wallet bindings are revoked (deposits from those addresses stop being credited), and shared previews are revoked. A key in an organization workspace belongs to the organization and keeps working, though any assignment to the deleted user is cleared.
- Stored files are erased — the content and the names both, since a file is your own data and no part of the marketplace's accounting.
- The remaining balance is forfeited — balances are not refundable, and while a deletion is pending the rails refuse to open a new deposit (a card checkout, a fresh wallet binding). A transfer sent on-chain from an already-bound wallet still lands, and is forfeited with the rest.
- Membership of organizations and providers ends. An organization with no other members is closed and its balance forfeited with yours; one with other members keeps its money, keys and workspaces. An account that is the sole admin of an organization with other members, or the sole owner of an approved provider listing, or the marketplace's last administrator, cannot request deletion until that role is handed over, and a scheduled deletion that would remove the last administrator when it falls due is withheld, so the account stays; the privacy page says so, and the request can be made again once the role is handed over. A pending or rejected provider application never blocks deletion.
- The email address becomes free again. A new account registered with it starts empty — no keys, no balance, no organizations, no link to the old account.
What remains is an anonymised account record: the ledger and usage history are append-only accounting, so they are kept, attributed to an opaque account id with the email, name, avatar and external logins removed. Nothing that remains identifies a person.
Practical posture
- Decide the policy floor — "no training on prompts", "declared retention only", "ZDR" — and encode it once as workspace routing defaults.
- Read the privacy policies of providers you pin (
only/order), not just the badges. - Send a
useridentifier when your own privacy design wants per-end-user attribution at the provider; omit it when it does not. - For auditability, keep
X-Generation-Ids alongside your own logs — they address every billing question after the fact.