Quantor
Security · 11 min read

Self-custody by construction — why your trading bot shouldn't hold your funds

In October 2022, 3Commas — at the time the largest crypto-bot product by paying users — confirmed that API keys for thousands of customers had leaked. Funds moved. Not all of them came back. The breach went through 3Commas's infrastructure, not the exchange. The exchanges were fine. 3Commas was the door, and the door was unlocked from the inside.

The takeaway most users wrote down at the time was something like "trading bots are dangerous, I should be more careful." The takeaway I wrote down was different: this entire class of failure is architecturally avoidable. You don't have to build the kind of product where a database leak moves user money. You just have to decide, on day zero, that your product never has the technical authority to move user money in the first place. Then the leak is embarrassing instead of catastrophic.

This post is about how Quantor is built so that even we can't drain a user's account. Not as a policy. As a structural property of the system. And it's about what that decision costs us — because it does cost something.

What "self-custody" means inside a bot SaaS

The term gets stretched in marketing. Three distinct meanings worth separating:

  • Hardware self-custody — your private keys live on a Ledger / Trezor. The bot doesn't apply. Different product category.
  • Custodial bot — you deposit funds with the bot operator. They hold the seed. They execute trades on your balance. They control the withdraw button. This is the category 3Commas / Stoic / similar lived in.
  • Non-custodial bot (Quantor's category) — your funds stay in your exchange account. The bot connects to the exchange via an API key whose scope you control. The bot can place orders but cannot withdraw. The bot operator has no path to your funds.

The third category is the architectural answer to the 3Commas class of incident. Even if the bot operator's database is fully compromised tomorrow, the worst the attacker can do with your stolen API key is place trades — they cannot move your money off-exchange, because the key doesn't have that permission to begin with. The exchange itself refuses the operation.

Self-custody in the bot context is less about you holding the key and more about the key not being capable of the operation that hurts you.

The anatomy of an API key (and what it can and can't do)

Every major spot exchange — Binance, OKX, Bybit, Bitget — supports multiple scope flags on an API key. The three that matter:

  • Read — balances, order history, market data. Required for the bot to know what it owns.
  • Trade / spot trading — place, cancel, modify orders against the user's balance, but only as orders that remain on the exchange. Funds never leave the exchange. This is the scope an automated execution product actually needs.
  • Withdraw — initiate transfers of crypto off-exchange to an external address. This is the dangerous one. Quantor never asks for it and you should never grant it to any bot.

Binance additionally supports an IP allowlist on keys. Quantor's onboarding flow surfaces the exact egress IP that our Cloud Run service uses (the load-balancer NAT range) and asks you to pin the key to those addresses. After that, even a leaked key fired from any other IP is rejected at the exchange edge before it touches your balance.

The full setup recipe in the onboarding doc:

# Binance → Account → API Management → Create API:
# 1. Label: "quantor-prod"
# 2. Permissions:
#      [x] Enable Reading
#      [x] Enable Spot & Margin Trading
#      [ ] Enable Withdrawals     ← LEAVE OFF
# 3. IP access restrictions:
#      [x] Restrict access to trusted IPs only
#      Add Quantor's egress range shown in mini-app
# 4. Two-factor confirmation on key creation.

That checklist is our security model on the exchange side. There is no fancier trick. We don't need one — the exchange's withdraw boundary is the strongest guarantee in the stack, and we make a point of leaning on it instead of working around it.

What our database actually stores

Inside Quantor, the encrypted exchange-key record contains three fields:

  • Encrypted blob — the API key + secret pair, encrypted with AES-GCM. The encryption master key does not live in the database. It lives in GCP Secret Manager, with access scoped to a single service account, audited, and rotatable independently of the database. The database operator does not have the master key.
  • Last-4 — the last four characters of the key, in plaintext, so we can show "…a3f7" in the mini-app without ever decrypting. Helps the user identify which key is which without ever exposing the secret to a UI.
  • Verified flag — boolean. Set to true only after the onboarding flow successfully called a read-only balance endpoint with the key. Catches copy-paste typos. Caches nothing else.

A database dump on its own is useless. An attacker would also need to compromise the Secret Manager service account, which is a separate trust boundary, in a separate GCP IAM domain, on a separate audit log. We treat those as independent failure events. Compromising both at the same time is the threshold below which our model of the world says "if this happens, the entire cloud provider security story has bigger problems than us".

And even if both fall — the attacker has API keys with no withdrawal scope, pinned to our egress IP. They can place spot orders on the user's balance from inside our infrastructure. Annoying, recoverable. They cannot move funds. That is the property we paid for by keeping the model boring.

How to verify all of this yourself

You don't have to take any of this on faith. Three checks any user can run in five minutes:

1. Inspect the scopes on Binance directly

On Binance, go to Account → API Management. Find the key labelled "quantor-prod" (or whatever you named it). Click Edit restrictions. The page shows exactly which scope flags are enabled.

  • If Enable Withdrawals is checked — go uncheck it immediately. Quantor never required it.
  • If Restrict access to trusted IPs only is off — also fix that. We publish our egress range and the onboarding flow shows it.

This is the only check that matters at the level of "can someone empty my account?" because the exchange, not Quantor, enforces these flags. The exchange is the source of truth for what your key can do.

2. Inspect Quantor's published egress range

curl -s https://quantorsaas.app/.well-known/egress-ips
# ["35.224.171.0/24", ...]

The same range you pinned your Binance key to. If the published list ever changes, we ship a notice in the mini-app and on /status with a window during which the old addresses still work — so the rotation is observable, not silent.

3. The "kill the company" test

Mental exercise. Suppose tomorrow morning Quantor disappears. Server, founder, repo — gone. What happens to your funds?

They sit in your exchange account, untouched, indistinguishable from any non-bot user. You still log in to Binance directly with your credentials. You still see your balance. You can withdraw, sell, sit on it — exactly as if Quantor had never existed.

The bot stopped. Nothing else stopped. That's the property "self-custody by construction" actually guarantees. A custodial bot that disappears tomorrow takes the seed with it; users wake up to "contact support" emails that never get a useful reply. The non-custodial bot disappears and the user shrugs.

Where the kill-switch fits in

Self-custody handles the "operator goes rogue or gets breached" threat. There is a different threat — "the bot is running a bad strategy, or markets are doing something it can't handle" — that the API-key scope doesn't address. That's what the runtime kill-switch is for.

The kill switch is a single boolean in Cloud Run env vars. When off, BotStartRiskPolicyService.assertCanStart() refuses to start any LIVE bot regardless of plan. When flipped mid-flight, running bots cancel open orders and stop submitting new ones at the end of the current tick. The flip takes effect within seconds, propagates without a redeploy, and is logged in the audit chain. Users see a banner.

Combined with the self-custody architecture, this gives us two independent stop layers:

  • Quantor breaks → exchange withdraw flag protects you. Worst case: leaked key with trade-only scope, recoverable.
  • Strategy breaks → kill switch + max-notional + regime gate protect you. Worst case: bounded loss within plan limits.

Neither requires us to be trustworthy. Both require us to be honest about what the architecture actually does — which is why we ship the build-signing endpoint and the public regime stream and this exact blog post.

What this costs us as a product

The non-custodial model is strictly more constrained than the custodial one. Things we explicitly cannot do that custodial bots can:

Features the architecture refuses
  • "Deposit $X with us and we'll manage it" — there is no Quantor balance. Users only ever hold funds on the exchange. We have nothing to onboard or off-board.
  • Cross-exchange rebalancing on a single click — would require withdraw scope to move funds between venues. We don't take that scope. If you want to rebalance, you do it yourself in two transfers between your exchange accounts.
  • "Yield" / lending vaults — would require either us holding funds (custodial) or us calling withdrawal functions on third-party contracts. Neither path exists. Vault yield is a custodial product in disguise; we don't ship it.
  • One-click "stop and refund" — we cannot refund what we don't hold. Subscription refunds are processed via the payment processor (LemonSqueezy / CryptoCloud / TG Stars). The exchange balance is yours and unaffected.

Each of these is a feature some users will ask for. Each is one we will refuse on principle, because they require us to acquire authority we deliberately chose not to have. The trade-off is explicit: smaller surface area, fewer features, dramatically smaller blast radius if anything goes wrong.

What we recommend

Independent of which bot you use — including if you don't use Quantor — the same two-step recipe protects you:

  1. Audit every API key in your exchange account today. Every active key, every scope, every IP restriction. Disable withdraw scope on anything that doesn't explicitly need it. If a "bot" demands withdraw scope, that is the signal to walk away from that bot.
  2. Treat the exchange's withdraw boundary as the primary firewall. Application-layer security is great; the exchange's "cannot withdraw" flag is better. It is enforced by a different organisation, on different infrastructure, with a different audit log. Use it.

That recipe is what makes the 3Commas-class of breach merely annoying instead of devastating. It's a 90-second change to your key permissions. It costs nothing. It would have substantially changed the outcome for thousands of people in October 2022.

The shorter version

Quantor never touches your funds. It cannot, by construction. Your exchange key has trade-only scope, our database stores only an encrypted blob, the encryption master key lives in a separately-administered Secret Manager, and the exchange itself refuses withdraw operations regardless of what our code thinks. If we lost every server tomorrow your money sits in your account exactly as it did this morning.

We're a small team. We do not have to be trustworthy. We just have to be small enough that we never had the authority you'd need to verify in a larger one.


For the operational details — exact AES-GCM mode, key rotation, audit-chain integration — see the Security page. The onboarding flow that walks you through Binance key creation lives at /how-it-works. Questions and criticism go to quantorsaas@gmail.com.