Last reviewed: 1 September 2026
You are being asked to hand a trading account to software written by one person. This page is what that person can tell you that is actually checkable — what the product can do with your key, what it structurally cannot, and where the gaps are. Claims you cannot verify are worth nothing here, so there are none.
PerpLog rejects a Bybit key that is allowed to trade. When you connect, we ask Bybit whether the key is read-only and refuse the connection unless it says yes — no account record is written. If the answer is missing or unexpected, the key is refused as well; an unclear answer never counts as a pass. The read-only set we need is Contract → Positions & Orders: read, plus Wallet: read.
That check is the second line, not the first. The first is that the code has nowhere to send an order: our Bybit client exposes two read methods — get_public and get_private — both hardcoded to HTTP GET, on a request path that has no parameter for a body; the seven Bybit endpoints we read (executions, order history, closed PnL, positions, wallet, account and key info) all go through those two. There is no order, cancel, amend, withdraw, or transfer call anywhere in the codebase — not disabled, not permission-gated, absent. Adding one would be a visible change to that client, not a configuration flag.
For Hyperliquid you give us a public wallet address and no key at all. It grants no control over funds; we query the same public API anyone can query.
Be clear about what a read-only key still exposes: your full trade history, positions, and balances. That is data worth protecting even though it cannot move money — which is what the rest of this page is about.
Key and secret are encrypted with AES-256-GCM before they reach the database, and only the encrypted form is stored. The encryption key is not stored with them: it is derived per user with HKDF-SHA256 from a master key that lives only in the server's environment, never in the database. A database dump on its own therefore decrypts nothing.
Because the derivation is per user, one user's key cannot decrypt another's. The derivation parameters are pinned by a test that exists to make an accidental change loud — changing them would silently make every stored credential unreadable.
The plaintext key is never sent back to the browser; the interface shows the last four characters. Every decryption is written to an audit log with the reason it happened, and known credential formats are redacted from log output.
Passwords are stored as bcrypt hashes, never in plaintext and never reversibly. A login attempt with an unknown email still performs one hash verification, so the response time does not reveal whether an address is registered.
Sessions use two HttpOnly cookies that JavaScript cannot read: a 15-minute access token and a 7-day refresh token scoped to the authentication path. Both are Secure and SameSite=Lax, and in production they carry the __Host- and __Secure- prefixes, which stop a subdomain from overwriting them.
Refresh tokens are stored as hashes, not as the tokens themselves, and they rotate on every use. All tokens from one login form a family: if an already-used token is presented again — the signature of a stolen token — the entire family is revoked and that session chain ends.
Separation is enforced by the database, not only by application code. Every tenant table carries a row-level security policy, and the role the application runs as returns zero rows when no user context is set. A query that forgets its owner check therefore returns nothing rather than someone else's trades.
This is tested against a real PostgreSQL instance in a dedicated CI lane, including a case that deliberately omits the application-level owner check to prove the database still holds the line. A coverage ratchet fails the build if a new table with an owner column ships unprotected.
All traffic runs over HTTPS. Both the API and the frontend send HSTS with a two-year max-age, a content security policy, frame-ancestors none, and nosniff. The database is not published to the internet at all — it is reachable only inside the server's private network.
The origin sits behind Cloudflare, and its firewall accepts ports 80 and 443 only from Cloudflare's address ranges, enforced in the packet-filter chain that actually governs container traffic. A direct connection to the server's IP times out. On top of that, a rate rule blocks bursts against login and registration.
The application rate-limits every sensitive endpoint separately — login, registration, password reset, sync, imports, AI calls. On the authentication endpoints the limiter is fail-closed: if its backing store is unavailable, requests are rejected rather than waved through.
The database is backed up daily and stored encrypted and off-site, using a tool that encrypts the repository itself, so the backup host never sees readable data. Retention keeps daily, weekly, monthly, and yearly snapshots.
A backup nobody has restored is a guess. A scheduled drill runs weekly: it restores the most recent snapshot and loads it into a throwaway database, so a truncated or unloadable dump surfaces as a failed job and an alert — not during an outage.
Limits worth naming: recovery restores to the last daily snapshot, so up to 24 hours of data can be lost. There is no point-in-time recovery and no cross-region replication.
Dependency updates arrive as automated pull requests across the Python, JavaScript, Docker, and CI-action ecosystems, grouped so that routine updates stay reviewable. Every build audits both dependency trees for known vulnerabilities and fails on high-severity findings, and a lock-coverage check prevents a dependency change from landing without a regenerated lockfile.
This section is here because its absence elsewhere is the tell. Nothing below is planned marketing; it is the current state.
If any of these is a dealbreaker for you, that is a reasonable conclusion to draw, and better drawn now than after you have connected an account.
If you find a security problem, please report it before disclosing it publicly, and you will get an answer. Email: security@perplog.app
Please include enough detail to reproduce the issue. There is no bug-bounty budget — what there is instead is a response, credit if you want it, and a fix.
This page is also referenced from our security.txt.
Related