§ 01
Three commitments, enforced in code
Trust is not a marketing word. Each of the three commitments below is implemented in this codebase today and is verifiable either in the public source (where applicable) or in a signed report we will produce on request. The "Hard rules" section below makes the implementation specifics concrete.
1. Privacy by default
A trader's personal trades are never visible to their organisation's admin. The data model enforces this at the query layer — an org admin cannot construct a query that returns another member's unaggregated trades. The contract is regression-tested in tests/test_snapshot_org_isolation.py and tests/test_trade_idor.py; any future commit that breaks it fails CI.
Broker API tokens are encrypted at the field level (see "Hard rule 3 — Encryption at rest" below). A database dump alone is not sufficient to recover broker access.
We do not sell, license, or syndicate customer trade data. We do not run third-party advertising. We do not feed trade data into unrelated machine-learning pipelines, our own or anyone else's. Behavioural pattern detection runs against your trade history, in your account, for your benefit. While processing is unrestricted, de-identified values may also contribute to cohort aggregates protected by publication floors; restriction excludes live and future contributions.
2. Founder-accessible support
The only support address is [email protected]. Varun (founder) reads it personally — there is no tier-1 outsourced inbox, no chatbot, no support ticket queue rotating between contractors. Acknowledgement SLA is 24 hours. For security reports the same SLA applies (see How to report below).
This commitment scales as we scale — when there is a second person in support, it will say so on this page, and the SLA will be defended by escalation, not eroded.
3. Public changelog and incident log
Every shipped change of consequence is logged at /changelog. Every incident worth a post-mortem is logged at /changelog#incidents with cause, blast radius, and the one specific thing that will not recur. The auto-rolled 30-day uptime grid + 90-day incident list at /status gives the reviewer a live-data version of the same commitment. We do not quietly fix and forget. If something we shipped affected your trading session, you will see it documented.
§ 02
Hard rules — enforced by code, not by promise
The rules in this section are the security properties we treat as non-negotiable. Each rule has a code path that implements it AND, where applicable, a CodeQL custom query that fails CI on regression — so a future contributor can't quietly weaken the property and pass review. The CodeQL queries live in .github/codeql/queries/and ship under the same MIT licence as the rest of the repository.
CodeQL itself is currently gated on the ENABLE_CODE_SCANNING repository variable — a private repo without GitHub Advanced Security can author the queries but not upload SARIF results. The queries still parse and run in CI on any push that flips the gate; the audit trail of "we authored the rule, here is its source" is the procurement-relevant claim.
1. Authentication — JWT signature, expiry, audience
JWT signature, expiration, and audience are validated on every authenticated request. The decoder is type-locked: a one-hour password-reset token cannot be replayed against /trades, /analytics, or any other bearer route. The single intentional verification bypass is app/security.py::revoke_token, which decodes an already-expired token to extract its jti for the blacklist. Every other call site is enforced by the custom CodeQL query tradeloop/jwt-verify-disabled.
2. HMAC verification — constant-time comparison
Every webhook signature comparison uses hmac.compare_digest. The canonical pattern lives in app/api/broker_webhooks.py::_verify_hmac and is paired with a 5-minute replay window (REPLAY_WINDOW_SECONDS = 300). A regression to == on any digest comparison is blocked by tradeloop/hmac-non-constant-compare. Per-org webhook signing secrets rotate via app/api/orgs.py; the secret is never logged or echoed.
3. Encryption at rest — Fernet under a dedicated key
Broker tokens are encrypted with Fernet (AES-128-CBC + HMAC-SHA256, authenticated) under a dedicated ENCRYPTION_KEY environment variable. The key derivation is SHA-256 over the encryption key, base64-urlsafe-encoded to Fernet's 32-byte requirement (app/crypto.py::_derive_key). When ENCRYPTION_KEY is unset the dev fallback is SECRET_KEY; production refuses to boot without an explicit ENCRYPTION_KEY (boot guard in app/config.py).
Rotating the JWT signing secret therefore does NOT corrupt stored broker tokens — the two keys are separate, and the custom CodeQL query tradeloop/fernet-key-from-secret blocks any new Fernet(settings.secret_key) callsite.
The KDF is intentionally a single SHA-256 (not PBKDF2 or HKDF) because the input is a high-entropy, server-managed secret rather than a user password. If a future audit calls for HKDF-Expand or PBKDF2, the swap is non-breaking (Fernet treats the key as opaque); we will document the change here in the same commit.
4. SSRF protection — outbound HTTP guarded
Outbound HTTP calls to user-supplied URLs (org outbound webhooks, MT5 server allowlists, custom broker callback URLs) are guarded against private-IP ranges (RFC 1918, link-local, and cloud-metadata addresses). The shared validator lives in app/services/url_safety.py; the MT5-specific guard lives in app/services/broker_service.py. The two canonical attack payloads — 169.254.169.254 (cloud metadata) and 127.0.0.1:6379 (in-VPC Redis pivot) — are explicitly tested in tests/test_org_webhook_ssrf.py and tests/test_mt5_ssrf_guard.py. Any new outbound HTTP path is checked by the custom CodeQL query tradeloop/ssrf-no-private-block.
5. Production safety — no SQLite in production
The boot sequence refuses to start when ENVIRONMENT=production AND DATABASE_URL contains sqlite (app/main.py::_assert_no_sqlite_in_production). Render's container disk is ephemeral — a production SQLite would silently lose every paid user on dyno restart. The guard fires at module import (before FastAPI is even constructed) so a misconfigured deploy crashes loud rather than serving 200s on an empty database. Tests run with ENVIRONMENT=testing via tests/conftest.py and are not affected.
6. Logging hygiene — credentials never reach the log shipper
Sentry's before_send scrubber strips JWTs, Bearer tokens, broker access tokens, Razorpay payment / order IDs, and India-specific PII (PAN, GSTIN, IFSC, 10-digit phone numbers with optional +91 / 91 country code) from event payloads before they leave the dyno. The scrubber lives in app/main.py::_scrub_string — open source, no proprietary regex hidden behind a vendor SDK. Authorization, Cookie, and X-CSRF-Token headers are always stripped regardless of pattern match.
The application convention is "if it's a credential, it doesn't go in logger.info". The Sentry scrubber is belt-and-braces; the convention is the first line of defence.
7. PII surface — third-party SDKs run through redaction
The same Sentry scrubber covers JWT, Bearer, email addresses, Razorpay IDs, PAN, GSTIN, IFSC, and Indian mobile numbers. PostHog and OpenAI calls run through their own redaction layer; the AI Coach prompt receives deterministic hashes and aggregated trade behaviour, not the user's email or name. New third-party SDKs go through the same review — the convention is documented in docs/agent/SECURITY_MODEL.md.
8. Compute integrity — engine modules, not free-form arithmetic
P&L, win-rate, exposure, drawdown, and every other number a regulator or compliance reviewer cares about is computed by a dedicated module in app/engine/. Engine modules are pure-compute (no I/O, no DB, no network), independently tested, and re-used across the API edge, the scheduler rituals, and the CSV export. If you're tempted to add a sum(t.pnl) inline in an API handler, the convention is "stop, write it as an engine call". The frontend's MCP rule (.cursor/rules/tradeloop.md) binds AI assistants to the same convention for traders.
9. Encryption in transit — TLS 1.3 + HSTS preload
TLS 1.3 with HSTS preload (max-age=63072000; includeSubDomains; preload) on production. Cloudflare Universal SSL fronts api.tradeloop.trade; Vercel handles app.tradeloop.trade. No mixed content. CSP is currently emitted in Report-Only mode while we tighten the policy from 'unsafe-inline' to nonce/hash; the violation ingest at /api/v1/csp-report is rate-limited to 200/min/IP and forwards to Sentry. Frame-ancestors, X-Content-Type-Options, Referrer-Policy, Permissions-Policy + COOP/CORP are documented in app/main.py::add_security_headers.
10. Admin gate — three layers + 2FA step-up
Admin access is defended by three layers: role=admin on the user row, FOUNDER_EMAILS belt-and-suspenders allowlist (even with role=admin the email must match), and ADMIN_IP_ALLOWLIST (CIDR list, network-level gate). Plus 2FA TOTP step-up (ADMIN_2FA_REQUIRED). Implementation in app/dependencies.py::require_admin.
§ 03
Maintenance proof — live, not hand-curated
A reviewer asking "is this project actually maintained?" should not have to trust a stale table of commit hashes. The public maintenance trail is split across two surfaces: release notes at /changelog and rolling service health at /status. Those are the surfaces we keep current; this page links to them instead of copying a list that can drift.
Release notes
Shipped changes, incident notes, and product-visible fixes live at /changelog.
Service history
The 30-day uptime grid and 90-day incident window live at /status.
Security gates
CI includes backend tests, type checks, CodeQL, Semgrep, gitleaks, Trivy, SBOM generation, and launch-readiness assertions before code reaches production.
If either public surface looks stale or inconsistent, email [email protected] — that is a product bug, not a documentation nit.
§ 04
How to report a vulnerability
Email [email protected] with the subject SECURITY: <one-line description>. We monitor this inbox personally. Please do not open a public GitHub issue for security vulnerabilities.
Our commitments
- Acknowledgement of receipt: 1 business day, 24h max.
- Triage (severity, scope): 3 business days.
- Fix shipped to production: 7 business days for high/critical, 30 days for medium.
- Coordinated public disclosure: within 90 days from initial report.
We don't run a paid bug-bounty programme yet. Confirmed reports are publicly credited at Acknowledgements (the "hall of fame" anchor) once the fix has shipped, with the reporter's permission.
§ 05
In scope
- The TradeLoop API (
https://api.tradeloop.trade/api/*) - The TradeLoop web app (
https://app.tradeloop.trade/*and any custom domain we operate) - The HMAC-signed inbound broker webhook endpoint
- The HMAC-signed outbound intervention webhook payload format
- The org-scoped data model — any cross-tenant data leak is treated as critical
- Authentication, session management, JWT handling
- Payment processing flow (Razorpay integration)
- Field-level encryption of broker tokens
- The public uptime / incidents endpoints under
/api/v1/status/*
§ 06
Out of scope
Reports about the following are acknowledged but not prioritised:
- Self-XSS that requires the user to paste arbitrary code
- Vulnerabilities in third-party services we depend on (Render, Vercel, Razorpay, Resend, Sentry, GitHub) — please report those directly to the vendor
- Issues only reproducible on outdated browsers (more than 2 versions behind)
- Rate-limit thresholds (those are policy, not a vulnerability)
- The OpenAPI schema being publicly readable at
/openapi.json— that's intentional in non-production environments only
§ 07
Severity rubric
We classify reports against the following rubric. The fix-time targets above scale with severity.
- Critical — cross-tenant data leak; unauthenticated remote code execution; payment-amount manipulation; mass account takeover.
- High — single-account takeover; PII leak through an authenticated endpoint; CSRF on a destructive action; HMAC bypass.
- Medium — local privilege escalation; rate-limit bypass on sensitive routes; reflected XSS.
- Low — verbose error messages leaking software versions; open-redirect; CSRF on a non-destructive action.
§ 08
Compliance roadmap (honest)
The hardest part of writing this section is not exaggerating. Our public security rule is simple: the only badges we'll display are ones we actually hold. The implementation-today claims live in the "Hard rules" section above; the targets below are dates we are genuinely working to.
- SOC 2 Type 1 — target Q3 2026.
- SOC 2 Type 2 — target Q1 2027.
- ISO 27001 — target 2027.
- Penetration test report — target Q3 2026 (third-party, redacted summary publishable).
- Bug bounty programme — target post-Series A; today reports are credited but not cash-rewarded.
Progress against these targets will be published only after real artifacts land. Until then, the public proof register reports only classified genuine evidence and does not display placeholder badges.
Today (implemented + tested)
- TLS 1.3 in transit, HSTS preload, no mixed content
- AES-256 at rest (Render Postgres managed encryption)
- Field-level Fernet encryption for broker tokens under a dedicated key (see "Hard rule 3")
- JWT type-locked to access-only on bearer authentication; reset/verify tokens cannot be replayed against the bearer routes (see "Hard rule 1")
- HMAC-signed inbound and outbound webhooks with a 5-minute replay window (see "Hard rule 2")
- Org-scoped data model — a trader's personal trades cannot be read by an org admin, enforced at the query layer
- Production fatal-rails on insecure-default
SECRET_KEY, Razorpay test keys, and missing Redis on multi-worker deploys - Audit log with HMAC chain
- Webhook signing-secret rotation API; secret never logged or echoed
- Sentry
before_sendPII scrubber strips JWT, Bearer tokens, email addresses, broker tokens, Razorpay payment IDs, PAN, GSTIN, IFSC, and Indian mobile numbers (see "Hard rule 6") - CodeQL custom queries enforcing four security hard rules in CI (
tradeloop/jwt-verify-disabled,tradeloop/hmac-non-constant-compare,tradeloop/fernet-key-from-secret,tradeloop/ssrf-no-private-block) - Public 30-day uptime grid + 90-day incident history at /status, sourced from a 5-minute internal probe sweep
§ 09
Acknowledgements (Hall of fame)
Researchers who have responsibly disclosed vulnerabilities are credited here once the fix ships and the embargo lifts. None to credit yet — once we have entries, the bullet list gains: the reporter's name (or chosen handle), a short description of the class of finding, the fix commit SHA, and the disclosure date. With the reporter's permission only.
The machine-readable disclosure metadata lives at /.well-known/security.txt per RFC 9116. Both the Contact and Policy directives in that file deep-link back to this page.
§ 10
Contact
Questions, scoping, or anything that doesn't fit a vulnerability report? Email [email protected].