Security at Bounty Operator

Where your code and keys go during a review, what is stored, which headers are in force, and how to report a vulnerability.

Last updated

Data flow: browser, Worker, provider

  1. 1Your browser

    Selects, checks, shows

    • You choose the files. The privacy check runs here, and the preview shows the request before it leaves the tab.
    • GitHub imports go straight to api.github.com.
    • Prompt export of a core profile ends here. Nothing is sent.
  2. 2Our Worker

    Verifies, adds the method, forgets

    • Checks your session, your allowance and the inputs again.
    • Adds the review method of the profile you chose, sends the request with your API key to one fixed endpoint of your provider and streams the answer back.
    • Holds the files, the key and the answer in memory for that one request. Writes none of them to storage or logs.
  3. 3Your provider

    Runs the model

    • Receives your files, your request, our review instructions and your API key.
    • Bills your key and keeps data under its own terms.

The review engine is one set of modules. The browser, the Worker and the MCP server import the same files, and they are in the public repository with the three core profiles. The method of every other profile is not in the repository: the Worker adds it when a review runs.

For a core profile the preview is the whole prompt your model receives. For a hosted profile it is the request: your focus, your context and the files, line by line. The Worker adds the method after that, and the provider you chose is the only other party that receives it. No page, download or API response returns it.

A review started from a coding agent takes the same path: run_review sends the files and your provider key to the Worker, and steps 2 and 3 are unchanged.

What is stored and what is not

Never stored

  • File contents and file names
  • Instructions and evidence notes
  • Model API keys and GitHub tokens
  • Review output
  • Raw IP addresses
  • Card details
  • Passkey private keys

Stored

  • A random account identifier
  • Passkey public keys, counters and labels
  • SHA-256 hashes of the recovery code, session tokens and connection tokens
  • Per review: status, profile, channel and time, for seven days
  • Stripe customer and subscription IDs, status, paid-until time and receipt email
  • A keyed hash of your IP address for rate limits, for up to a day
  • Daily page and funnel totals with no account identifier

The privacy policy has the full list with retention times.

Headers and CSP in force

The Worker sets these on every response, pages and API alike:

Response headers
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.github.com https://openrouter.ai; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
Cross-Origin-Opener-Policy: same-origin
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy: camera=(), microphone=(), geolocation=()

What the policy means in practice:

  • Scripts and styles load from this origin only. There is no inline script, no inline style and no eval, so injected markup has nothing to run.

  • The browser can connect to three hosts: this one, api.github.com for imports and openrouter.ai for the OpenRouter connection.

  • The site cannot be framed, and forms post to this origin only.

  • Model output and pasted text are treated as untrusted. The app writes them with textContent and never parses them as markup. Downloaded packets have remote images and raw HTML neutralised.

Check it yourself:

Terminal
curl -sI https://bountyoperator.com/ | grep -i -E "content-security|strict-transport|cross-origin|x-frame|referrer|x-content|permissions"

Passkey-only accounts

  • Passkeys only

    No password exists to phish, reuse or leak. Sign-up asks for no email address. User verification is required on every sign-in.

  • Secrets stored as hashes

    Session tokens, connection tokens and the recovery code are 256-bit random values. We keep their SHA-256.

  • Hardened cookies

    The session cookie is __Host-prefixed, HttpOnly, Secure and SameSite=Lax. It lasts up to 30 days.

  • Origin and CSRF checks

    A state-changing request from the browser has to come from this origin. Signed-in requests also carry a per-session CSRF token.

  • Fresh passkey for sensitive actions

    Rotating the recovery code, adding or removing a passkey, creating a connection token and deleting the account need a passkey check from the last 10 minutes.

  • Recovery resets everything

    Using the recovery code replaces it and signs out every session and connection token. Removing a passkey signs out the other sessions.

Tokens, provider calls and billing

Connection tokens

A connection token starts with bok_ and lets an AI client do two things: read your usage and run a review. It cannot reach billing, passkeys, recovery or deletion. A token lasts 90 days, an account holds three, and revoking one takes effect at once.

Provider calls

Each provider has one fixed endpoint. The Worker refuses redirects, stops when the provider has been silent for 180 seconds, and caps the answer at 2 MB. Your API key is used for that request and is redacted from any error message.

Hosted profiles

The Worker reads the answer to a hosted profile as it streams, in memory, before it reaches you. An answer that repeats the method of the profile is stopped and the call ends with the code output_withheld. A file or a request that asks the model for its instructions is treated as data.

Input checks

A review takes up to 50 text files and 240 KB in total. Before anything is sent, the engine scans every line for API keys, access tokens, wallet keys and seed phrases, and blocks the request when it finds one. Email addresses and IP addresses are warnings you confirm. A finding names the file, the line and the kind of match, and never the matched text.

Billing

Payment runs on Stripe Checkout, so card details never touch our application. Webhooks are signature-checked, and paid access is recomputed from Stripe’s current state on every event, including refunds and disputes.

Report a vulnerability

Email security@bountyoperator.com.

Or use GitHub private vulnerability reporting. Both reach the maintainers privately.

Include:

  • the affected surface, and the version or the time of the request
  • a minimal reproduction
  • the impact: what an attacker reads, changes or spends
  • a proposed fix, if you have one

Remove secrets and other people’s data from anything you attach.

In scope

The hosted site
The Worker and its API routes, passkey sign-in and recovery, sessions and CSRF, connection tokens, quota and concurrency limits, billing state, the MCP endpoint, the content security policy and the static pages.
The review engine
Input checks, request assembly, review parsing and packet rendering. A way to make model output or pasted text execute script, load a remote resource or leave the page is a vulnerability.
The MCP server
The bounty-operator-mcp package and the endpoint at /api/mcp.
The CLI
The bounty-operator-kit Python package: the sanitizer, the ai-review request path, file reads and writes.

Testing rules

  • Use accounts you created. Leave other accounts alone.
  • Keep request volume low. No denial-of-service testing and no automated scanning of the production site.
  • Do not test payments against the live checkout. The billing logic runs locally with billing disabled.
  • Stop at the first proof of a problem and report it.

The full policy is in SECURITY.md. The machine-readable contact is at /.well-known/security.txt.

Source

The source is public under the MIT licence: the Worker, the review engine with its three core profiles, the free tools, the MCP server and the command-line kit. The gauntlet stages and the panel cross-examination run on the hosted service. The licences page lists every dependency. The changelog lists what shipped and when.

A Worker built from the repository runs the hosted profiles on short stand-in instructions, and its /api/health reads "profiles": "community". On this site it reads "profiles": "hosted".