Coarena by CoastyCoarenaby Coasty
LeaderboardBlog
Coarenaby Coasty

Real-world evals for computer-use agents. Live tasks, blind human judgment, and every number published with the rule that produced it.

Arena

  • Play
  • Leaderboard
  • Benchmark

Evidence

  • Dataset
  • Metrics API

About

  • Mission
  • Governance
  • Blog

Legal

  • Terms
  • Privacy

© 2026 Coasty Systems, Inc.

Every claim on this site cites the file that keeps it

Privacy

CoArena Privacy Policy

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.


1. Notice at Collection

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).


2. Who we are and how to reach us

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.


3. Categories of personal information, and where each comes from

3.1 Account identity — from Google

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.

3.2 The consent record

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.

3.3 The coarena_judge cookie

A 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.

3.4 The link between the cookie and your account

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.

3.5 IP address

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.

3.6 Activity event log

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).

3.7 Page-view analytics

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.

3.8 Passive judging telemetry — collected without asking you anything

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.

3.9 Tasks you write

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.

3.10 Votes, annotations and step verdicts

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.

3.11 Your judge quality profile

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.

3.12 Screenshots and page text — the material exposure

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.

3.13 Buyer contact details

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.


4. Retention, by category

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.


5. Who receives personal information

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.


6. Screenshots: exactly what is masked, and what is not

This is the section most likely to matter to you, so it states the code's four outcomes rather than a summary of them.

6.1 How detection works, and its defining limit

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).

6.2 The four outcomes, as 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).

6.3 The raw frame always goes to the model provider

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.

6.4 What the detector structurally cannot see

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.

6.5 Access controls on frames

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.


7. Sensitive personal information

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:

  • Account log-in credentials. Password inputs are in the masking taxonomy and are masked where a DOM exists (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.
  • Precise geolocation. We derive none. A map or address rendered on a page can appear in a frame.
  • Racial or ethnic origin, religious belief, health, sex life, union membership. Not collected, not inferred, not used. Can appear in a frame if a task drives an agent to a page displaying it.
  • Biometrics. Faces can appear in frames. No face detection is run and no face template has ever been derived, and the code pins that as a type-level constant that must stay false (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.


8. Do we sell or share personal information?

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:

  • `sample` (free, no key, no account required — a public disclosure, not a sale): task prompts, blind preference outcomes, the first three actions per run, agent and outcome.
  • `preference`: the above uncapped, plus model reasoning traces and annotations with judge counts.
  • `trajectory`: the above plus tool observations, run errors, and no screenshots and no screenshot paths — withheld at every tier (see 6.5).
  • `eval`: the above plus your passive judging behaviour and unreduced battle internals.

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.


9. Your rights, and exactly how each is handled today

All requests go to founders@coasty.ai (src/lib/consent.ts:355). Read each subsection for whether a mechanism exists.

9.1 Right to know / access (§1798.110, §1798.115)

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.

9.2 Right to delete (§1798.105)

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:

  • Tasks you submitted are driven to consent level 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).
  • Labels — your votes, annotations and step verdicts — are retained and indexed as withdrawn. They are kept deliberately, so the published discard rate stays recomputable from the same table it describes (src/app/api/me/withdraw/route.ts:29-33).
  • [NOT BUILT — the index is not yet read.] This section used to say those labels are "filtered out of every aggregation path". They are not. 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.
  • Deliveries already made to a licensee cannot be recalled by this route, and the route says so in its own response body (src/app/api/me/withdraw/route.ts:119-123).
  • Your `users` row is not deleted. Nothing in 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.

9.3 Right to correct (§1798.106)

[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.

9.4 Right to opt out of sale (§1798.120)

[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.

9.5 Right to limit use of sensitive personal information (§1798.121)

[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.

9.6 Right to non-discrimination (§1798.125)

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).

9.7 Authorised agents

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.

9.8 Appeals

[NOT BUILT] There is no appeals workflow. Reply to the decision email and a human will re-review it.


10. How we verify a request

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.


11. Global Privacy Control and other opt-out preference signals

[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.


12. Minors

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.


13. People who are not CoArena users but appear in the data

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.

  • The one rule of posting, stated at sign-in and on /terms, is: never point a task at anything private or logged-in. We enforce it socially, not technically.
  • Frames are never anonymously fetchable (§6.5).
  • Every frame is stamped with the basis that decided its outcome, so a recipient can gate on 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.


14. Security

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.


15. International users

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.


16. Changes to this policy

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.


Appendix A — Contact

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.


Appendix B — What shipping this policy requires (for the repository, not for readers)

B.1 The page

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:

  • default export 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;
  • import 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;
  • import DISPLAY, MICRO, MONO from @/components/boards/tokens and reuse the LINK class and Section component pattern from src/app/terms/page.tsx.

B.2 The footer link — exact change

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.

B.3 Three other places that enumerate public pages

  • 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".

B.4 One live inconsistency this policy does not inherit

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.

B.5 What §9.4 and §11 would need in order to stop saying NOT BUILT

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.

B.6 Facts an operator must fill in before publication

  • The hosting provider (§5) — the code does not name it.
  • The active COARENA_SANDBOX backend (§5).
  • Whether a postal address is to be published (Appendix A).
  • Whether any licence key has been issued and any licensed download has occurred. This policy deliberately makes no numeric claim about that, because a count is a fact about the database, not about the code, and it changes without a commit.