How this is built

Security and your data

Last updated: 28 August 2026 · This page describes how the product works today, not how we intend it to work

You are handing a website your investment history, so you are entitled to know what happens to it. This page says what we store, what we never see, how the account is protected and where it all runs. Where something is weaker than you might assume, it is on this page too.

We never touch your broker account

There is no broker login anywhere in the product. You download an export from your broker yourself and upload that file. We never ask for a broker password, we hold no token and no API key for your account, and no part of the code can place an order, move money or read your balance at the broker. This is a property of how the product is built, not a promise about our behaviour: the connection does not exist.

What we keep from an export

A broker export is a wide table. We read a fixed set of columns and store one row per transaction with these fields: the date and time, the type, the ISIN, the ticker symbol, the product name, the quantity, the price, the currency, the amount, the amount converted to euros, the fees, the exchange rate used, and one free-text field. That last field is the description line your broker prints on the row, such as a dividend note or a corporate action, and it is cut off at 500 characters.

Every row passes a validation step before it is stored. An ISIN that does not have the shape of an ISIN is not kept as text but replaced by a key derived from the ticker. A currency code that is not a valid ISO 4217 code is emptied instead of stored. Names and notes are trimmed and stripped of control characters. Dates outside a sane window are pulled back inside it. The point of that step is to keep a strange export from breaking the calculation, and the side effect is that unusual content in a column does not survive into the database intact.

If you also upload a portfolio overview, the snapshot of what you hold today, we keep five things per line: the instrument key, the name, the quantity, the price and the value. Every other column in that file is ignored.

The file itself is not kept. It is parsed in memory and dropped; what stays behind is the file name, the broker it was recognized as, the number of rows and any warnings, so your upload history is readable.

What we never receive

We never receive a broker password or a broker login. Card details go to Stripe and never reach our servers. The product never asks for your name, your address, your date of birth or a bank account number, and the importers do not read those columns even when a broker prints them in the file. The only personal detail the account needs is an email address, alongside your language and currency preference.

There is one exception worth naming. For a file format we do not recognize, you map the columns yourself once. That mapper stores exactly the columns you point it at, so if you point it at a column holding something personal, that is what gets stored.

Passwords and sessions

You can sign in in three ways: with Google, with an email address and a password, or with a sign-in link we email you, which means no password at all. If you do set a password, it is stored as a PBKDF2-HMAC-SHA256 hash with 480,000 iterations and a fresh random salt per password. The password itself is written down nowhere. The iteration count sits inside the hash, so it can be raised later without invalidating anyone: an old hash is replaced silently the next time that person signs in. The minimum length is 10 characters, and one IP address gets 25 password attempts per hour.

A session is a signed cookie. It is HttpOnly, so no script can read it, it is SameSite=Lax, and it is marked Secure whenever the site runs over https, which in production it always does. It lasts 180 days and is refreshed while you keep using the app. The cookie also carries a counter that belongs to your account, and signing out raises that counter, so signing out ends every session you have anywhere and not just the one in front of you.

In transit and in the browser

The site is served over https. It sends Strict-Transport-Security with a one-year max-age including subdomains, so a browser that has seen the site once will not try plain http again. The content security policy allows scripts from our own domain only, which is why there is no third-party script on the site or in the dashboard. Framing is refused through both X-Frame-Options and frame-ancestors, content-type sniffing is off, the referrer is trimmed on cross-site navigation, and geolocation, microphone and camera are denied. Two things leave the page for another party. The typeface is fetched from Google Fonts, and browser errors go to Sentry in its European region, so a page that breaks for you is visible to us. Sentry is named in the privacy policy with the other processors.

Where it runs

The API, both scheduled jobs and the database with its volume run on Railway in EU West Metal, Amsterdam (europe-west4-drams3a), and have done since 28 August 2026. The website is served by Vercel, the network sits behind Cloudflare, payments run through Stripe and email through Resend. Each of those and what it processes is listed in the privacy policy.

What a share link shows, and what it does not

A share link is optional and off until you switch it on. When you do, a random token becomes the address of a read-only page; switching it off makes that address invalid immediately.

Visitors see your positions and how they performed, not your cash balance or your deposits. It is worth being exact about what that sentence covers. The page hides your cash balance, it hides every deposit you have made, and it hides the totals that are computed from them. It does not hide the value of each position: every position is shared with its amount in euros, so anyone holding the link can add those amounts up and arrive at the value of your invested portfolio. If that number is something you would rather not publish, do not switch the link on.

The preview image that chat apps and social networks show for a share link carries percentages only.

Taking your data out, and deleting it

Export is one button under Account settings. It hands you everything as JSON: your account record, every stored transaction with all of its fields, your upload history and your settings.

Deleting is one button as well, with a typed confirmation. It removes your transactions, uploads, saved column mappings, portfolio snapshots, opening positions, settings and the account itself. Two small registers survive on purpose, and both hold nothing but an irreversible salted hash of your email address plus the date a free trial started. One of them is cleaned up after a year; the other is kept, because a register that expired would hand the same person an unlimited series of free trials. Neither can be turned back into an address.

Sign-ups and abuse

Registration with a disposable email domain is refused, against a blocklist of 8,335 domains. Forwarding and alias services are checked first and explicitly allowed: Apple Hide My Email, Firefox Relay, DuckDuckGo, SimpleLogin and addy.io all work, because the people who use them are exactly the people this page is written for.

What we do not do with your data

There are no ads and we do not sell data. There is no third-party analytics service, no tracking pixel and no advertising cookie anywhere on the site; the content security policy above would block one if it were added by accident. Sentry receives error reports, never page views. What we do measure is six milestones per account, such as that a first file was uploaded and that the comparison was shown, recorded on our own servers as a milestone and a date. Your subscription is what pays for this.

Reporting a security problem

Write to security@bullbenchmark.com with what you found and how to reproduce it. That address is read by a person and you will get an answer. Two requests: please do not run automated scans against the live service, and please do not use another person's account or data to demonstrate a finding. A description is enough for us to reproduce it ourselves.

Questions that are not security findings go to support@bullbenchmark.com. What we do with personal data in legal terms is in the privacy policy; the agreement itself is in the terms of service.