Skip to content

Key & secret management

Every key the platform holds, what an attacker could do with it, and how to rotate it.

Threat analysis: security model. Fund-safety guarantees: custody.

Blast-radius table

Ordered by severity.

KeyAddressWorst caseRotation
Relayer owner0x75a8b522FC3195e3a3570F11f111AC89c0D35975_authorizeUpgrade on 22 chains — replace the implementation and bypass every guaranteetransferOwnership, per chain
Autopilot owner BACKEND_PRIVATE_KEY0x2Ed05570214f6C0F7612B580aB37C163076e0162UUPS-upgrade the Autopilot vault; set fees; curate routerstransferOwnership
DCA owner = executor0x5935745431D1385ae1a9CE2644fb5f5d1f140459UUPS-upgrade the DCA vaulttransferOwnership
TX_SIGNER / POL_TX_SIGNERSame as relayer ownerMake the relayer .call an arbitrary target — spending the attacker's own fundssetSigner(address) — immediate, per chain
PRIVATE_KEY_EXECUTOR0x5935…0459Force bad-slippage churn on agents' own positions, bounded by liquidity and the router allow-listsetExecutor(address) on both vaults
PRESALE_MASTER_SEEDDerive presale walletsRe-seed
CLIPPERS_PAYOUT_PRIVATE_KEYDrain the creator payout walletReplace
Orderly ed25519 keysPer userTrade a user's Orderly account. Cannot withdraw — that needs a fresh wallet signatureRegenerate per account
ZeroDev session keysPer dApp projectScoped operations on a user's smart account. User-revocableUser revokes
Provider API keysQuota abuse, costRotate at the provider
LLM keysCostRotate at the provider

The three things that are wrong today

1. Owner keys are EOAs, not multisigs

0x75a8b522… holds upgrade authority over the relayer on 22 chains — every interactive trade on the platform. 0x2Ed05570… holds it over the Autopilot vault. A single compromised private key can replace either implementation and bypass every custody guarantee documented elsewhere in this site.

Moving both to multisigs is an explicit launch blocker for promoting Autopilot to users, and is overdue for the relayer.

2. PRIVATE_KEY_EXECUTOR doubles as the DCA vault owner

So a compromised executor key does not merely mean bad trades — it means upgrade authority over the DCA vault. setExecutor already exists, so separating them is a single owner transaction plus a config change. It has not been done.

3. copytrade-agent has committed live secrets

config.json in the external repo (~/Documents/code/personal-agents/copytrade-agent) contains a live Telegram bot token, a Helius API key, and a Hyperliquid API-wallet private key — committed to git history.

Rotating all three and blanking the file is step zero (CT-0) of that workstream. Nothing else there should start first. See copy trading.

Where secrets live

LocationContents
VM /home/bisht/bz-backend/.envEverything server-side
VM /home/bisht/bz-agent/.envLLM credentials, database URLs
Netlify env vars per siteVITE_SITE, VITE_API_URL, VITE_GA_MEASUREMENT_ID only
MongoDB, KMS-encryptedOrderly ed25519 private keys
Nowhereswap-sdk ships with zero embedded keys

.env never leaves the VM, and never arrives on it

deploy.gcp.sh excludes it from the rsync in both directions. That is deliberate — but it means a new variable has to be added on the VM by hand:

bash
./scripts/deploy.gcp.sh ssh
nano ~/bz-backend/.env
pm2 restart bz-backend --update-env

"The code deployed but the feature does not work" is almost always this. UNISWAP_API_KEY is the canonical case.

What cannot leak into a bundle

Only VITE_-prefixed variables reach the frontend bundle

And every one of them is public by construction. Never add a secret to a VITE_ variable, .env.ai, netlify.toml or netlify.ai.toml.

The reason swap-sdk can safely run in a browser is that anything key-bearing either goes through a backend proxy or is injected via configure() with a key that is safe client-side.

Rotation procedures

TX_SIGNER — do this immediately on suspicion

bash
# per chain, from the owner
setSigner(newSignerAddress)

Then update TX_SIGNER (and POL_TX_SIGNER for chain 137) in the VM .env and restart.

Why this is urgent but not catastrophic

An attacker with TX_SIGNER can make the relayer .call an arbitrary target — but funds are pulled from msg.sender by transferFrom, so they spend their own money. The realistic damage is griefing and reputational, not theft of user balances.

setSigner exists precisely so you rotate first and investigate afterwards. Do not wait for confirmation.

Order matters: rotate on-chain first, then update .env. The reverse leaves the backend signing with a key the contract no longer accepts, so every swap fails.

PRIVATE_KEY_EXECUTOR

bash
# on the DCA vault (from its owner)
setExecutor(newExecutorAddress)
# on the Autopilot vault (from its owner)
setExecutor(newExecutorAddress)

Then update .env, fund the new wallet with gas on each automation chain, and restart.

Fund the new executor before switching

Both crons stop silently when the executor has no gas — see monitoring.

Contract owner → multisig

bash
transferOwnership(multisigAddress)

Per chain for the relayer (22 transactions). Verify with scripts/check-contract-owner.js afterwards.

Test the multisig can upgrade before transferring

Once ownership moves, _authorizeUpgrade requires the multisig. A misconfigured multisig means the contract can never be upgraded again.

Provider and LLM keys

Rotate at the provider, update .env, restart. No contract interaction.

For a key that also has a registered referrer (1inch's 0x5222…1D35, Squid's integrator id, OpenOcean's referrer), check whether the referrer registration is tied to the key.

Orderly ed25519 keys

Per user, regenerated through POST /defi/orderly/add-orderly-key. Encrypted at rest with AWS_KEY_ID_KMS.

This is the one key Blazpay holds on a user's behalf

It is scoped to Orderly's API — it can trade but cannot withdraw, because Orderly's withdrawal flow requires a fresh wallet signature. Still, treat models/orderlyAccount.js as sensitive data, and rotating AWS_KEY_ID_KMS means re-encrypting every stored key.

ZeroDev session keys

User-controlled, not platform-controlled

POST /v1/dapp/account/:projectId/revoke-session lets a user revoke unilaterally, after which their smart account is entirely self-controlled and Blazpay's automation stops. This is the correct posture and the UI should say so at setup, not in a footer.

IndexNow key

Not a secret — it is deliberately public at public/{key}.txt. But it has a sharp edge:

Never rotate and submit in the same deploy

IndexNow caches failed key validations for hours or days and returns 403 until they expire. public/{key}.txt must match KEY in scripts/indexnow-submit.mjs, and the file must be deployed and reachable before any submission. Current key: fbaf800f3098a10a73e20691138e2590. It has been rotated once already.

Auditing what is in use

bash
cd bz-backend
grep -rhoE 'process\.env\.[A-Z0-9_]+' --include='*.js' \
  controllers services utils middleware v1 routes index.js \
  | sed 's/process\.env\.//' | sort -u

Exclude archive/, new-backend/, microservices-backend/ and gateway/ — they are not mounted and contribute variables nothing reads.

bash
# what is actually set on the VM?
./scripts/deploy.gcp.sh ssh
grep -oE '^[A-Z0-9_]+' ~/bz-backend/.env | sort

Diffing those two lists finds both unused variables and missing ones.

Hygiene rules

RuleWhy
Never commit a .envThe copytrade-agent situation is what happens when this slips
Never log a key, even truncatedLog lines end up in PM2 files and screenshots
swap-sdk stays key-freeIt ships to the browser
Rotate on-chain before updating .envThe reverse breaks every swap
One key, one purposeThe executor/DCA-owner overlap is the counter-example
Fund a new signer before switchingSilent cron failure otherwise

Emergency contact points

SituationFirst action
TX_SIGNER suspected leakedsetSigner on every chain — now, not after investigating
Executor suspected leakedsetRouters([...], false) on Autopilot to halt all routing, then setExecutor
Owner key suspected leakedpause() all three contracts if you still control the key; otherwise there is no lever — this is why multisigs matter
Provider key leakedRotate at the provider; check for quota abuse
A user reports an unauthorised tradeCheck TradeExecuted events against the agent's policyHash; the event carries it deliberately

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.