Privacy
Effective date: August 6, 2026 Last updated: August 6, 2026
Coasty Systems, Inc. ("Coasty", "we", "us") operates CoArena at coarena.ai. The legal person behind the product is named in code at src/lib/consent.ts:117 (OWNER_LEGAL_NAME); "CoArena" is a product name, not a party.
This policy describes the personal information CoArena actually collects, what the running code actually does with it, and which of the rights described below are backed by a mechanism you can use today versus handled by a person reading email. Where a control does not exist, this policy says [NOT BUILT] and names the only channel that does exist. It never describes a workflow we intend to build as though it were running.
Where this policy sits in the stack. The versioned sign-in consent record and the licence you grant are at https://coarena.ai/terms, which TERMS_OF_SERVICE.md §2 designates as the controlling surface for anything the two documents both address. This policy is the collection-and-rights notice. If this policy and /terms disagree about the licence, /terms controls; if they disagree about a factual description of processing, the code controls and both documents are wrong until corrected.
We collect the categories below. This section is the CCPA Notice at Collection (Cal. Civ. Code §1798.100(a)); §§3–5 give the detail.
| CCPA category (§1798.140(v)(1)) | Collected | What, concretely | |---|---|---| | (A) Identifiers | Yes | Google account email, display name, avatar URL, Google subject id; the coarena_judge cookie id; IP address (held in memory only, persisted only as a salted hash) | | (B) Cal. Customer Records categories | Yes | Name and email address — the same fields as (A) | | (C) Protected classifications | Not collected by us | May appear incidentally inside a screenshot; see §6 | | (D) Commercial information | Not collected about you | Order, booking and receipt pages may appear inside a screenshot; see §6 | | (E) Biometric information | No | Faces may appear in captured frames. No face detection is run and no face template has ever been derived (src/lib/redact.ts:799-806, src/lib/redact.ts:873-881 — faceTemplateDerived: false) | | (F) Internet or other network activity | Yes | Page-view events on a closed route list; the activity event log; passive judging telemetry | | (G) Geolocation data | No | Nothing in src/ derives location from an IP or from any header. No geolocation library is present | | (H) Sensory data | Yes — the largest category | Screenshots of the pages and desktops the agents drove, and accessibility (text) snapshots of those pages | | (I) Professional / employment information | Not collected | May appear incidentally inside a screenshot | | (J) Education information | Not collected | May appear incidentally inside a screenshot | | (K) Inferences / profiles | Yes | A per-judge quality and trust profile derived from your own labelling behaviour | | Sensitive personal information (§1798.140(ae)) | Yes, incidentally | See §7 |
Purposes. Operating the arena; creating and maintaining your account; running the battles you post; attributing and scoring the labels you produce; rate limiting and abuse prevention; measuring the sign-up funnel; and assembling and licensing datasets to AI labs. Dataset licensing is a sale — see §8.
Retention. Stated per category in §4. The honest summary: almost nothing is deleted. No pruning job exists anywhere in src/. The only DELETE FROM statement against personal data in the entire application deletes a single queue-serve event row (src/lib/db.ts:2223).
Email founders@coasty.ai. This address is the constant CONSENT_WITHDRAWAL.channel in src/lib/consent.ts:355, and it is the address rendered on /terms. It is the only privacy channel that exists.
[NOT BUILT] There is no privacy request form, no toll-free number, no ticketing system and no automated intake. A request is a human reading mail.
Email address, display name, avatar image URL, the OAuth provider string and the Google subject id, plus created/last-seen timestamps. Written by upsertUser from the next-auth jwt callback (src/auth.ts:86-100; supabase/migrations/0001_init.sql:238-253). Source: a third party — Google — at your direction when you press the sign-in button. Email is mandatory: signIn() returns false for a profile without one (src/auth.ts:82-84).
Sign-in is Google-only. We never see your Google password.
Which consent version your account accepted and when (users.consent_version, users.consent_at), an append-only consent_granted event, and a byte-exact archive of the sentence you were shown (consent_versions, supabase/migrations/0001_init.sql:344-354). Source: automatic, derived from the act of signing in (src/auth.ts:108-118).
A note on the version identifiers: they look like dates and are not dates. All seven were minted in one sitting as a forward sequence (src/lib/consent.ts:29-57). The authoritative timestamp of your agreement is users.consent_at, never the version string.
coarena_judge cookieA random UUID in a first-party cookie, one-year max-age, sameSite=lax, and deliberately not httpOnly and not secure (src/app/api/_lib/judge.ts:17-22; src/lib/judging.ts:94-96). It is minted server-side on your first qualifying request, before and independent of any account.
We describe this cookie internally as "not PII" because its *value* is random. That is not the legal position and this policy does not take it: a persistent identifier that can be linked to a consumer or household is personal information under §1798.140(v)(1)(A), and this one is the join key for everything in §3.4, §3.7 and §3.11.
At sign-in, adoptJudgeId runs UPDATE … SET user_id = … WHERE judge_id = … AND user_id IS NULL across battles, votes, annotations and events (src/lib/db.ts:2438-2451, called at src/auth.ts:101-104).
Read that consequence plainly: activity recorded before you had an account — including page views logged on the funnel before you ever signed in — is retroactively attached to your named account the moment that browser signs in. Nothing unlinks it. There is no "browse anonymously and stay anonymous" state once you sign in from the same browser.
Taken N hops from the right of X-Forwarded-For (src/lib/clientip.ts:27-40). It is held raw only in process memory — the ipHits maps in src/lib/ratelimit.ts:30 and src/app/api/access-request/route.ts:19 — and neither map is persisted. It is persisted only as `hashIp()`, the first 16 hex characters of sha256(AUTH_SECRET + ':' + ip) (src/lib/db.ts:2453-2462), in judge_rate_events.ip_hash (src/app/api/_lib/label-rate.ts:44,72) and inside one battle_created event's metadata (src/app/api/battles/route.ts:237-246). logEvent's contract forbids raw IPs (src/lib/db.ts:1804-1805).
We do not claim this hash is anonymous. A 16-hex truncation over the IPv4 space is reversible by brute force if the salt leaks. It is pseudonymous.
One row per recorded action — page_view, signin, consent_granted, battle_created, battle_pooled, battle_shared, battle_unshared, run_continued, vote_cast, annotation_added, export_downloaded, consent_withdrawn, plus judge_telemetry and operational types — carrying user_id, judge_id, battle_id and a capped, secret-scrubbed metadata blob (supabase/migrations/0001_init.sql:255-263; src/lib/db.ts:1793-1839; src/lib/events.ts:11-160).
First-party only. The beacon posts to our own origin and writes to our own database (src/app/api/view/route.ts). It records: a path matched against a closed allowlist of seven routes — anything else is stored as the literal string "other" (src/app/api/view/route.ts:53-61,110) — the referrer's host only, and the utm campaign tag.
Enforced in the route, not by convention: no IP, no user agent, no full URL, no screen size, and the public result card /b/<id> is never logged at all.
There is no third-party analytics, session-replay, advertising or tag-management code in this application. package.json contains no such dependency, and src/app/layout.tsx loads exactly one inline <script>, a JSON-LD block (src/app/layout.tsx:125-131). No pixel fires before consent because no pixel fires at all.
For each judge and battle: time to vote, whether you scrubbed the replay, how deep you scrubbed, play count, which steps you paused on, and which output you copied (src/app/api/battles/[id]/telemetry/route.ts:3-16,34-40; stored as a judge_telemetry event, src/lib/db.ts:1864-1876,1917-1943).
The route calls itself "the anti-form" — it is collected around your vote without a single question. This telemetry is delivered to licensees as the record's `behavior` block (src/app/api/export/route.ts:29-36) at the eval tier (src/lib/licensing.ts:119-127). It is behavioural data about an identified person, collected silently and sold onward. We are naming it here because it is the category a shorter policy would leave out.
The task prompt (free text), an optional starting URL, a derived category (supabase/migrations/0001_init.sql:100-106). Source: you.
Your prompt is sent verbatim to the third-party model provider running the agent (src/lib/agents/runner.ts:479) and is delivered to licensees as task.prompt (src/lib/dataset/schema.ts:274-278).
The prompt itself is not run through a PII redactor on the export path. The derived instantiation bag is redacted (src/app/api/export/route.ts:1001) because it is a PII bag by construction; the prompt is not. Do not put anything in a prompt you are unwilling to license.
Preference labels (votes.winner, .answer_winner, .route_winner), free-text notes, magnitude and justification in vote_metadata, typed annotations, and per-step verdicts with their own free-text justification (supabase/migrations/0001_init.sql:208-236,412-446). Source: you.
What leaves: the preference label leaves; annotations leave aggregated as {type, label, judges: <count>, confirmed: <bool>} (src/lib/dataset/schema.ts:180-185) with no judge id; step-level judgments leave in the judged block (src/lib/dataset/schema.ts:361-372).
What does not leave today: your free-text vote note and justification are not assigned anywhere in the delivered record. We will not promise they never will be — the delivered schema sets additionalProperties: true (src/lib/dataset/schema.ts:288), so adding a field is a one-line change. If that changes, this policy changes with it.
judge_calibration (calls, correct), gold_responses (which gold item, what you answered, whether you passed), judge_trust (a state and a free-text reason), label_retractions and retracted_labels (supabase/migrations/0001_init.sql:453-536).
This is an evaluative profile about an identified person, keyed to both your cookie and your account, and it is a gate, not just a score (supabase/migrations/0001_init.sql:500). Retracted rows are retained on purpose so the published discard rate stays recomputable (supabase/migrations/0001_init.sql:487-489,514-516). Only aggregate quality statistics reach licensees, never a per-judge profile.
One raw frame per step, plus a separate masked file when the detector produced a proven mask (supabase/migrations/0001_init.sql:170-177,190-198), and a gzipped accessibility (text) snapshot of the page (steps.a11y_path). §6 describes what is masked and what is not. Read §6 before deciding what to point a task at.
The a11y snapshot is the page's rendered text — a superset of what a screenshot shows — and it is not covered by the frame-masking pipeline. The compressed file has no serving route of its own (src/app/api/dataset/_lib/evaluate.ts:1509; src/app/api/dataset/checkers/route.ts:192), but its text does reach licensees, decompressed, scrubbed of agent-typed input and run through the text redactor, inside verification.substrate when a page assertion produced the verdict (src/app/api/dataset/_lib/evaluate.ts:1219-1234).
Artifacts — files the agent produced or scraped (markdown, CSV, JSON, text) — carry whatever was on the page. They are redacted at *write* time, not at delivery: content = blindProse(redactPII(input.content)) (src/lib/artifacts.ts:238). Metadata ships at lower tiers; the redacted body ships inline from the trajectory tier upward (src/app/api/export/route.ts:239-254).
URLs. The page each action landed on ships redacted (redactUrl, src/app/api/export/route.ts:516), but the registrable domain is delivered unredacted at every tier, deliberately (src/app/api/export/route.ts:508-511). Which sites your task took an agent to is always visible to a recipient.
If you request dataset access we store your organisation, email, use case and requested tier (access_requests, supabase/migrations/0001_init.sql:319-326; src/app/api/access-request/route.ts:34-39). Source: you. Purpose: sales. There is no payment path in the application — a human mints a key once terms are signed.
There is no retention schedule implemented anywhere in this codebase. The following is the actual behaviour.
| Data | Retention as implemented | |---|---| | Account row (users) | Indefinite. No code path deletes a users row | | Consent record and archive | Indefinite, by design — consent_versions rows are inserted once and never updated (supabase/migrations/0001_init.sql:342-343) | | coarena_judge cookie | 1 year in your browser (src/lib/judging.ts:96). Indefinite in every table it was written into | | Sign-in session (JWT) | 30 days (src/auth.ts:59,79) | | Raw IP | Transient — in memory, pruned per window, never persisted | | judge_rate_events.ip_hash | Indefinite. One row per label write, forever; persistence across deploys is the stated point (supabase/migrations/0001_init.sql:578-579) | | Activity events, page views, judging telemetry | Indefinite. No pruning job exists | | Tasks, votes, annotations, step verdicts | Indefinite. Withdrawal reclassifies; it does not delete (§9.2) | | Judge calibration / trust / retractions | Indefinite by design — "a deleted label cannot be audited" (supabase/migrations/0001_init.sql:438-441) | | Screenshots, masked frames, a11y snapshots, artifacts | Indefinite | | Records already delivered to a licensee | Outside our database entirely. Governed by the licence, not by us |
[NOT BUILT] We do not currently have the code to enforce any retention limit. Setting one is a product change, not a policy edit.
Google LLC — as identity provider. In this role it is the *source* of your account identity, not a recipient of anything we generate (src/auth.ts:86-100). Google is separately a *recipient* in its model-provider role, immediately below.
Model providers: Anthropic, OpenAI and Google — the agents' inference backends (src/lib/agents/providers/{anthropic,openai,gemini}.ts; roster at src/lib/db.ts:57-92). They receive your prompt verbatim and the raw, unmasked screenshot on every step. See §6.3.
One named retention term exists in this codebase: the fable-5 agent requires 30-day data retention on the Anthropic organisation behind our API key — every request 400s under zero-data-retention — and runs safety classifiers that can refuse a run mid-flight (src/lib/db.ts:59-62).
Daytona — the remote sandbox control plane, when it is the selected backend (src/lib/agents/sandboxes/daytona-control.ts:34). Which backend runs is an environment setting, COARENA_SANDBOX (src/lib/agents/sandbox.ts:17,134); the alternatives ship no data off this host. The operator must confirm the production setting before publication and, if it is Daytona, keep this entry.
Supabase — managed Postgres for every table above and a private Storage bucket for frames and artifacts (src/lib/storage.ts:16,42-48,109-110).
Our application hosting platform. The repository documents Render for this role (DEPLOY.md, "Option B", and the Render-specific operational notes at DEPLOY.md:255,418) and the funnel beacon reasons about "Render's proxy" (src/app/api/view/route.ts:68). The application code does not name its own host — src/lib/origin.ts:1-28 deliberately refuses to derive an origin from the deployment — so the operator must confirm the current provider before this policy is published, and name it here.
Dataset licensees (AI labs) — see §8.
Anyone on the internet, for the public sample. GET /api/export with no Authorization header returns the sample tier — a capped, stratified selection carrying task prompts, blind preference outcomes and the first three actions per run — and it requires no account and no key (src/app/api/export/route.ts:1070-1078,1165-1171; src/lib/licensing.ts:79-88). The route states the rule: "the account gate is on producing data, never on consuming it" (src/app/api/export/route.ts:1200-1203). The sample carries no screenshots, no reasoning traces and no judge behaviour (src/lib/licensing.ts:87).
Anyone with the link, for a result card you publish. /b/<id> renders your task prompt and the outcome to whoever opens it, with no account (src/app/b/[id]/page.tsx:1-4,208). It carries none of the trajectory. It exists only for a battle you affirmatively shared, and it is deliberately absent from the sitemap, llms.txt and any index (src/app/sitemap.ts:30-38).
Operators, via the admin backup. GET /api/admin/backup streams every base table in the public schema — including users — as NDJSON, gated only by a bearer ADMIN_TOKEN (src/app/api/admin/backup/route.ts:18-23,64-72). Frame bytes are not in it.
This is the section most likely to matter to you, so it states the code's four outcomes rather than a summary of them.
Detection is structural, not OCR and not a vision model: we ask the page for the geometry of its own PII-bearing elements — form controls carrying a non-empty value (including password, email and tel inputs) and text nodes matching the published text taxonomy — and composite opaque rectangles into the captured image. Every rectangle is then sampled in the encoded output and confirmed to be the mask colour before the frame is publishable (src/lib/redact.ts:791-821; src/lib/agents/framepii.ts).
Where there is no DOM there is no detection (src/lib/agents/framepii.ts:13-14).
redactedFileFor implements them (src/lib/frames.ts:60-83)1. Mask found, applied and verified → the masked image is delivered. 2. Detector ran and found nothing → the original capture is delivered unmodified, labelled unmasked-no-regions-detected (src/lib/frames.ts:70-72). That scan is the only thing standing behind it. 3. Desktop battle — whole-screen capture, no DOM → no mask is attempted and the original capture is delivered unmodified, labelled unmasked-desktop-no-dom (src/lib/frames.ts:80-82). 4. Detector error, unverifiable mask, capture error, nothing attempted, or no receipt at all → the frame is refused outright and nobody receives it (src/lib/frames.ts:66,83).
So: a delivered screenshot can carry whatever was on the screen — names, addresses, account details, a face — either because the detector looked and found nothing, or because on a desktop it could not look at all. Two of the four outcomes deliver unmodified pixels by design. This policy will not say "screenshots are redacted before delivery"; that sentence is true of one branch out of four.
Every response carries x-coarena-frame-variant, x-coarena-frame-detector and x-coarena-frame-redaction-basis headers so a recipient can tell which branch produced the bytes in front of them (src/lib/frames.ts:168-175).
Redaction protects the delivery, not the inference call. The raw capture is base64-encoded and sent to the model API as the agent's observation on every step, and the executor is explicit that this "must not depend on anything the redactor does" — redaction runs only *after* the raw frame is stored (src/lib/agents/executor.ts:716-724). No sentence in this policy claims personal information is masked before it leaves Coasty; for this path it is not.
Published in full at src/lib/redact.ts:824-860 and on /api/dataset/manifest. Among them: every frame of a desktop battle in whole; text baked into images; anything drawn to <canvas> or WebGL; video frames; PDFs in the browser viewer; content inside any <iframe>; closed shadow roots; CSS-generated text; and PII split across adjacent inline elements — a card number rendered as <span>4111</span><span>1111</span> matches no single text node, so the frame is delivered as a clean page while the text record, which sees the same content joined, does mask it.
The text scrubber's own out-of-scope list (src/lib/redact.ts:680-691) includes personal names, postal addresses, unlabelled alphanumeric identifiers, and "credentials, API keys and session tokens appearing as opaque strings".
No residual-PII audit has been run. residualAudit is {residualRate: null, sampleSize: 0, auditedAt: null} (src/lib/redact.ts:696) because no human has audited a held-out sample of delivered text. We publish the zero rather than a flattering estimate.
Screenshots are not licensed to anyone, at any tier. A licence key opens no frame: GET /api/screenshots/... and GET /api/artifacts/... refuse every key, with or without ?raw=1, and return the reason (src/app/api/screenshots/[...path]/route.ts, const keyed = false). Frame *paths* ship at no tier either — every path field in a delivered record is null and carries preScreenshotBasis: "withheld_not_licensed" (src/app/api/export/route.ts), because 2,633 paths are themselves an index of the archive even when the bytes are unreachable.
The reason is the capture surface rather than the redactor. Agents navigate autonomously, so the sites photographed are not the sites a submitter named: 14 domains appear in task URLs and 139 were actually rendered and captured. Our component-rights ledger holds no licence in third-party page content, so there is no tier at which those images are ours to sell.
The one credential that still returns frame bytes is a short-lived, battle-scoped token minted by the battle detail page for a signed-in viewer — so a submitter can watch their own replay and a judge can see what they are judging. That is display inside the product, not delivery. The battle detail API requires an account (src/app/api/battles/[id]/route.ts), and there is no allowlist beyond that: any signed-in Google account qualifies.
The battle detail API requires an account (src/app/api/battles/[id]/route.ts:46). There is no allowlist beyond that: any signed-in Google account qualifies.
We do not ask you for sensitive personal information and we collect none of it by design. It can nonetheless be present, incidentally, in a captured frame or a page-text snapshot:
src/lib/redact.ts:816). They are not masked on a desktop frame, and the text scrubber explicitly does not catch credentials that appear as opaque strings.src/lib/redact.ts:799-806, src/lib/redact.ts:873-881).Because such information can end up inside a licensed record, we treat the right to limit the use and disclosure of sensitive personal information as available to you (§9.5) rather than arguing that an exemption applies.
Yes. We sell it. We say "licence" in our commercial documents; under the CCPA the transaction is a sale and we are not going to argue the point.
Cal. Civ. Code §1798.140(ad) defines a sale as disclosing personal information to a third party for monetary or other valuable consideration. CoArena's entire business is disclosing tasks, trajectories, preference labels, screenshots and judging telemetry to AI labs for money (src/lib/licensing.ts:3-9, src/lib/licensing.ts:59-127). Calling that a licence changes the contract, not the statute.
Categories disclosed, by tier (src/lib/licensing.ts:78-127). The three keyed tiers are the sale; the sample tier is given away for nothing, which makes it a public disclosure rather than a sale — and, because it needs no account and no key, a broader one:
Categories NOT sold. Your email, name, avatar or Google id are not in the delivered record schema — it has no contributor-identity field (src/lib/dataset/schema.ts:269-341) and toBattle() never reads user_id or judge_id (src/lib/db.ts:158-180). Your coarena_judge id is carried internally in the corpus builder but exposed to a buyer only as an integer judges count (src/lib/dataset/corpus.ts:70-71,396-397; src/lib/dataset/schema.ts:185,395). Your free-text vote note and justification are not delivered today (§3.10).
We do not "share" personal information as the CCPA defines that term (disclosure for cross-context behavioural advertising). We run no advertising, no ad tech, and no third-party analytics — see §3.7.
We have not sold personal information of consumers we know to be under 16 (§12), because we collect no age signal at all and therefore know of no such consumer. That sentence is a statement about our knowledge, not a control.
A note on delivery already made. Once a record has been delivered to a licensee, our database cannot recall it. §9 says what we can and cannot do about that.
All requests go to founders@coasty.ai (src/lib/consent.ts:355). Read each subsection for whether a mechanism exists.
You may request the categories and specific pieces of personal information we have collected about you, the sources, the purposes, and the categories of third parties.
What exists in code: GET /api/me returns your account, your judge id, your points, and your last 20 activity events (src/app/api/me/route.ts:20,34). That is a history panel, not an access response.
[NOT BUILT] There is no data-portability export of everything we hold about a person. GET /api/admin/backup dumps whole tables and is not a per-consumer tool. Fulfilling an access request today means an operator hand-assembling it. We commit to the statutory 45 days (extendable by 45 more with notice); nothing in the code enforces or tracks that clock.
Read this subsection carefully. There is no deletion pipeline, and we are not going to imply one.
What exists in code is withdrawal, not erasure — POST /api/me/withdraw, src/app/api/me/withdraw/route.ts. It requires you to be signed in, acts on both your account and the judge cookie that account adopted, and it does the following:
private, which is one-way. That is a real removal from the sellable corpus, including the free public sample (src/app/api/me/withdraw/route.ts:26-29).src/app/api/me/withdraw/route.ts:29-33).getWithdrawnJudgeIds and getWithdrawnUserIds exist in src/lib/licensing.ts and have no callers, so a vote you cast on somebody else's still-licensable battle continues to ship with that battle (src/app/api/export/route.ts reads listVotesForBattle with no judge filter). Withdrawal removes your TASKS from delivery today; wiring those two predicates into the export and aggregation paths is the outstanding work, and it is named here rather than described as done.src/app/api/me/withdraw/route.ts:119-123).src/ deletes one.[NOT BUILT — no user interface. The endpoint has zero callers in the application. There is no button, link or form anywhere in the product that invokes it; a repository-wide search for its required confirmation string finds only the route itself. Today, withdrawal in practice means emailing founders@coasty.ai and an operator acting on your behalf. /terms says the same thing in its own words: "parts of the removal path are still manual."**]
[NOT BUILT] Erasure of the account record, the event log, the telemetry, the rate-limit ledger and the judge-quality profile has no implementation at all. If you ask us to delete those, an operator will do it by hand against the database.
The SLAs we actually publish, from src/lib/consent.ts:364-367: within 7 days your contributions leave the corpus, so every delivery from then on ships without them; within 30 days we notify buyers who already received your data and issue a replacement snapshot. The second number is a notification and re-issuance commitment, not a guarantee that a buyer deletes their copy. We cannot compel that, and we will not write a sentence suggesting we can.
[NOT BUILT] There is no correction mechanism in the codebase — no endpoint, no admin tool, no field-level edit path for personal information. Corrections are made by hand by an operator, on request to founders@coasty.ai.
Note two structural limits: (a) your account identity is written from Google on every sign-in (src/auth.ts:92-98), so correcting name, email or avatar means correcting them at Google; (b) archived consent text is append-only by design and is never rewritten (src/lib/consent.ts:22-24) — it is the verbatim record of what you were shown.
[NOT BUILT — no "Do Not Sell or Share My Personal Information" link exists. There is no such link in the footer (src/app/layout.tsx:237-244), no preference page, and no opt-out flag on any table or on users. Appendix B.5 names the change required.**]
The closest existing mechanism is the withdrawal described in §9.2: it drives every task you submitted out of every tier, including the free sample, and indexes your labels as withdrawn (the index is recorded but not yet read — see §9.2). It is one-way and it is not scoped — there is no way today to keep participating while opting out of the sale, because participation and the licence are the same act (src/lib/consent.ts:3-8).
Until an opt-out link ships, email founders@coasty.ai and we will run the withdrawal for you.
[NOT BUILT] No mechanism exists. Because sensitive information reaches us only incidentally inside a frame or a page-text snapshot (§7), the only control we can actually apply is to remove the affected records — the same manual path as §9.2. Email founders@coasty.ai and identify the battle or the frame.
We will not deny you service, charge you a different price, or provide you a different level of quality because you exercised a right.
We must be straight about one interaction: withdrawal drives your tasks to private and indexes your labels out. That is the *content* of the request, not a penalty for making it. But it is one-way, and it does reduce what your account shows you. There is no financial incentive programme — we pay nothing for data and offer no discount for it — so no Notice of Financial Incentive is given. Points earned in the arena are a local counter and confer no monetary value (src/components/battle/points.ts:1).
You may use an authorised agent. [NOT BUILT] There is no agent-verification workflow; send written authorisation to founders@coasty.ai and we will verify your identity directly.
[NOT BUILT] There is no appeals workflow. Reply to the decision email and a human will re-review it.
If you have an account: sign in with the same Google account and send the request from that Google email address. This is the only verification method the code enforces anywhere — POST /api/me/withdraw calls requireSignIn and acts on the identity in the session, never on an identifier supplied in the body (src/app/api/me/withdraw/route.ts:77-79). Because users.email is UNIQUE (supabase/migrations/0001_init.sql:238-253), matching your sending address to that column identifies you to a reasonable degree of certainty.
If you have no account — for example you appear in a screenshot: see §13. We cannot verify you against a record we cannot find, and we will tell you so rather than deleting the wrong thing.
[NOT BUILT] We do not use a third-party identity-verification service and do not ask for government identification. We will never ask you for a password, and we do not have one.
[NOT BUILT — WE DO NOT CURRENTLY HONOUR GPC.] Nothing in this codebase reads Sec-GPC or navigator.globalPrivacyControl; a repository-wide search for either returns no match. A browser sending the signal today changes nothing about how CoArena behaves.
We are stating this rather than omitting it, because California regulations require a business that sells personal information to process opt-out preference signals, and a silent policy is worse than an honest gap. Appendix B.5 names the change required.
CoArena is not directed to children. Sign-in is Google-only, and Google's own terms require account holders to meet a minimum age.
[NOT BUILT] We collect no age signal of any kind. There is no birth date field, no age gate, no age attestation and nothing in src/ that reads an age from the OAuth profile. We therefore have no technical means of determining whether a user is under 16, and we do not knowingly sell the personal information of consumers under 16. We do not have actual knowledge of any such consumer, because we have no way to acquire that knowledge.
The CCPA requires opt-in consent before selling the personal information of a consumer aged 13–15, and parental consent for those under 13. We have not implemented either flow. If you believe a person under 16 has an account, email founders@coasty.ai and we will withdraw and remove the account's contributions.
This section exists because the honest answer is uncomfortable.
Agents drive real websites. A captured frame or page-text snapshot can therefore contain personal information about people who never visited CoArena, never signed in, and never consented to anything — a name in a review, an address on an order page, a face in a photograph, another person's message in an inbox. Section 6 explains why our masking does not reliably catch it: on a desktop battle nothing is masked at all, and on a page the detector misses everything in §6.4.
What we do about it.
desktop_no_dom and refuse unscanned captures.What we cannot do, stated plainly.
[NOT BUILT] We have no way to search the corpus for a person. There is no OCR over frames, no face index, no name index, and no reverse lookup from a person to the frames they appear in. If you tell us you appear in a screenshot, we can act only on information you can give us that maps to a record we can find — a battle id, a result-card link, a URL the agent visited, an approximate date. Without that, we will tell you we could not locate the records rather than claim we deleted them.
Your rights are still your rights. If you are a California resident and your personal information is in our systems, §9 applies to you whether or not you have an account. Write to founders@coasty.ai. Requests from non-users are handled entirely by hand.
[NOT BUILT] There is no notice mechanism by which a person appearing in a frame learns that they are in it. We do not know who they are.
Frames and artifacts live in a private Supabase Storage bucket, created non-public, and every read is streamed through an authenticating route rather than a signed URL (src/lib/storage.ts:14-22,42-48). Event metadata is passed through a secret scrubber on write (src/lib/db.ts:1794,1817-1820). Database errors have the Supabase project host stripped before they surface (src/lib/pg.ts:80-88). Sessions are 30-day JWTs (src/auth.ts:59,79).
The coarena_judge cookie is deliberately readable by client script and is sent without secure (src/app/api/_lib/judge.ts:11-21). It is not a credential and grants no access; the sign-in session is what authorises anything.
No method of transmission or storage is completely secure.
CoArena is operated from the United States and all data is processed there. /terms §05 and §06 carry the GDPR/UK GDPR lawful-basis analysis and the Canadian (PIPEDA) position; they are not restated here. This policy is the CCPA notice.
We will update this policy when the processing it describes changes. The effective date at the top is the authority.
[NOT BUILT] There is no mechanism that notifies you of a change to this policy. No email is sent; no in-product banner is tied to it. The UpdateBanner component (src/components/UpdateBanner.tsx) detects a new *build*, not a new policy.
The one notification mechanism that does exist is the consent version bump: if the sentence shown at sign-in changes, CONSENT_TEXT_SHA256 no longer matches, verifyConsentTextIntegrity() fails, and the manifest stops claiming consent until a new version identifier is minted and every user re-accepts (src/lib/consent.ts:154-161; verifyConsentTextIntegrity at src/lib/licensing.ts:893). That mechanism guards the *consent sentence*, not this policy. A material change to this policy that widens the sale should be accompanied by a consent version bump, and per src/lib/consent.ts:52-56 the next identifier must not continue the date-shaped sequence — mint v8 or a counter-suffixed string.
Coasty Systems, Inc. Privacy requests: founders@coasty.ai
[NOT BUILT] No postal address, toll-free number or web form is published for privacy requests. Email is the only channel.
Create `src/app/privacy/page.tsx` — in the App Router this single file *is* both the route and the page component, exactly as /terms and /governance are (src/app/terms/page.tsx, src/app/governance/page.tsx; note /governance additionally has a sibling claims.ts, and /governance/croissant.json is a route.ts — neither pattern is needed here).
Mirror src/app/terms/page.tsx:1-41:
PrivacyPage();export const metadata: Metadata with a bare title: "Privacy" (the root layout's title.template appends " — Coarena"), a description under ~155 characters, alternates: { canonical: "/privacy" }, and an explicit openGraph block — a page without one inherits the root's values and previews as the homepage;CONSENT_WITHDRAWAL and OWNER_LEGAL_NAME from @/lib/consent and render channel, removeFromCorpusDays and alreadyDeliveredDays from the constants, never as copied literals, so §9.2 cannot drift from src/lib/consent.ts;DISPLAY, MICRO, MONO from @/components/boards/tokens and reuse the LINK class and Section component pattern from src/app/terms/page.tsx.src/app/layout.tsx, the tuple array inside the footer <nav aria-label="Footer"> at lines 237–244 (entries at 239–243). Currently:
``tsx ["/", "Arena"], ["/leaderboard", "Leaderboard"], ["/data", "Data"], ["/governance", "Governance"], ["/terms", "Terms"], ``
Add one entry after Terms:
``tsx ["/privacy", "Privacy"], ``
Nothing else in the footer changes — the .map() at src/app/layout.tsx:245-254 renders any tuple added to the array. Six links still fit the md:flex-nowrap row; verify at 768px and drop md:flex-nowrap if they do not.
If a "Do Not Sell or Share My Personal Information" link is required (§9.4) it is a second footer entry, not a sub-link of Privacy — see B.5.
src/app/sitemap.ts:61-62 — add { url: ${siteUrl}/privacy, changeFrequency: "monthly", priority: 0.3 } beside the /terms entry.src/app/llms.txt/route.ts:41-48 — add a - [Privacy](${o}/privacy): … bullet beside the Governance and Terms bullets.src/app/api/view/route.ts:53-61 — add "/privacy" to KNOWN_PATHS, or every view of this page is logged as the literal string "other".src/app/api/export/route.ts:195-204 still tells a reader that desktop frames carry redaction.basis: "not_attempted" and that a consumer "must gate them on that basis exactly as on detector_error". The code stamps desktop_no_dom (src/lib/agents/framepii.ts:212) and serves those frames (src/lib/frames.ts:80-82), which is the opposite of detector_error. The buyer-facing surfaces have been corrected — src/app/governance/claims.ts:324, src/app/api/dataset/manifest/route.ts:663, src/app/api/dataset/_lib/actionmap.ts:977, src/lib/redact.ts:832 and src/app/data/DataGated.tsx:85 all now say desktop_no_dom and all now say the frames are served. The export route's header comment is the last stale copy. It should be corrected in the same change that ships this policy so no document in the repository contradicts §6.2.
These are the changes that would make the opt-out and GPC sentences true. None of them exists today, and this policy does not describe any of them as running.
1. A stored opt-out flag. A migration adding users.sale_opt_out_at bigint and a judge_sale_opt_out(judge_id, created_at) table, so the preference survives for a visitor who has never signed in — the same both-identities rule src/app/api/me/withdraw/route.ts:17-22 already applies. 2. An enforcement point. src/app/api/export/route.ts filters on getLicensableTaskIds() (src/app/api/export/route.ts:1096; src/lib/licensing.ts:782). The opt-out has to join there, and in the aggregation paths src/lib/licensing.ts exposes via getWithdrawnJudgeIds / getWithdrawnUserIds (src/lib/licensing.ts:1213-1218). Without an enforcement point, a flag is a UI element, not an opt-out. 3. A GPC reader. A helper alongside src/lib/clientip.ts that reads the Sec-GPC: 1 request header, plus a client check of navigator.globalPrivacyControl, wired to set the flag from (1) on first request. Nothing in the repository reads either signal today. 4. A `/do-not-sell` route — src/app/do-not-sell/page.tsx, same shape as B.1 — plus the second footer tuple in src/app/layout.tsx:237-244, plus "/do-not-sell" in KNOWN_PATHS (src/app/api/view/route.ts:53-61). 5. A withdrawal button. POST /api/me/withdraw exists and has zero callers. A confirm-dialog control on src/app/account/page.tsx posting { "confirm": "withdraw my contributions" } would turn §9.2's manual path into a real self-service one and let /terms drop its "no self-serve button yet" sentence.
COARENA_SANDBOX backend (§5).