What this page is and isn’t
Is: the floor for everyone who builds a bot that calls the Limitless API or signs orders with a private key. Read once before Module 01, revisit the day you ship anything that holds funds.
Isn’t: a substitute for a security audit, a smart-contract review, or a bug-bounty programme. If your bot is going to manage other people’s funds, hire a professional, this page is the personal-trader floor, not the institutional ceiling.
Section 01
The threat model.
Six threats account for almost every story of a personal trading bot losing money. None of them require a state-actor adversary, they happen routinely to careful people who skip one step.
A .env file or hardcoded secret committed to a public repo, or even a private one that later goes public. Bots scrape GitHub continuously; keys are exfiltrated within minutes. Defence: Section 02 + a pre-commit hook.
An attacker reads PRIVATE_KEY from a leaked log, an unsecured deploy panel, or a compromised laptop. Defence: burner wallets with capped balances (Section 03) and on-chain caps via approval limits.
Your bot signs an EIP-712 order; the signed payload is captured in a log or proxied request and replayed by an attacker. Defence: include a nonce + a tight expiry in every signature (Section 05); never log signed payloads.
A bug or a flawed strategy causes the bot to place repeated orders or hammer the API. Defence: idempotency keys on orders (Section 05), per-iteration spend caps (Module 14 in the academy), a panel that shows you in real time (Module 02).
Your Alchemy/Infura key gets exfiltrated, or an npm/PyPI dependency is hijacked. Defence: pin versions, audit dependencies, never curl | sh, and rotate RPC keys quarterly even when nothing’s wrong.
Your trader control panel is reachable from the public internet without authentication. Anyone who finds it can cancel your orders, or worse, trip a kill switch at the worst moment. Defence: HTTP Basic auth or a token query parameter (Module 02), never “security through obscurity.”
Section 02
Secrets hygiene.
Every secret in this curriculum lives in environment variables or a vault, never in source, never in a log line, never in a stored signed payload. Three habits keep this honest: the .env pattern, a strict .gitignore, and a pre-commit hook that fails loud if a secret slips through.
The .env + .env.example split
.env holds real values and is gitignored. .env.example is committed; same keys, dummy values, so a teammate (or future-you) knows what variables the project expects.
# .env.example (commit this)
LIMITLESS_API_KEY=REPLACE_WITH_TOKEN_ID
PRIVATE_KEY=0xREPLACE
RPC_URL=https://mainnet.base.org
PANEL_TOKEN=REPLACE_WITH_RANDOM
Strict .gitignore
# Secrets
.env
.env.*
!.env.example
*.pem
*.key
private_key.txt
# Trace artefacts (can leak signed payloads)
orders.log
fills.log
# Local dev databases
*.sqlite
*.db
The ! prefix re-includes .env.example after the wildcard ignores everything else.
Pre-commit hook, fail loud if a secret slips through
#!/usr/bin/env bash
# Block obvious secret leaks. Not a substitute for hygiene; a backstop.
PATTERNS='0x[a-fA-F0-9]{64}|sk-[A-Za-z0-9]{20,}'
HITS=$(git diff --cached -U0 | grep -E "$PATTERNS" | grep -v '^-')
if [ -n "$HITS" ]; then
echo "REFUSED: candidate secret in staged diff."
echo "$HITS" | head -5
exit 1
fi
exit 0
For team-wide enforcement, use gitleaks or trufflehog in CI, the local hook is the first line, CI is the second.
A secret committed to git is permanent.
Even after git reset + force-push, the value is in the reflog and in any clone. Rotate immediately, do not try to clean history first. Section 07 has the rotation playbook.
Section 03
Wallet posture.
The single most expensive mistake in this curriculum is pointing a bot at a wallet that holds more than you can lose in a bad afternoon. Three rules: burner only, smallest workable balance, top-up not pre-fund.
Burner only
A dedicated MetaMask account (or freshly generated key) that holds nothing else. No NFTs, no other tokens, no other dapp permissions. If the key leaks, the blast radius is the balance, nothing more.
Smallest workable balance
Fund with the smallest amount that lets the bot place real orders, usually $20–$50 of USDC on Base, plus a dollar or two of ETH on Base for gas. The balance is the worst-case loss; treat it as the budget for the entire learning loop.
Top up, don’t pre-fund
When the burner runs low, top it up by hand from your main wallet. Never set a recurring transfer or grant the bot any access to your main wallet. The friction is the feature.
Never reuse a key across environments.
Dev, staging, and prod each get their own burner. If the dev key leaks via a bad commit, prod is untouched. Sharing keys across environments is one of the most common ways a small mistake becomes a total loss.
Section 04
Deploy secrets.
The moment your bot runs anywhere other than your laptop, secrets need to live somewhere remote, and never in your image, your repo, or your logs. Module 01 walks the Railway path concretely; the rules below apply on any platform.
Where secrets live in production
- –Railway / Vercel / Fly: the platform’s built-in environment-variable UI. Encrypted at rest, injected into the process at boot.
- –Bare VPS: a system service file (/etc/systemd/system/….env) with chmod 600 and ownership restricted to the service user.
- –Bigger setups: a managed secrets manager, AWS Secrets Manager, Doppler, Infisical, with rotation and audit logs built in.
Where they must never live
- –A Dockerfile ENV directive (image layers are forever).
- –A build artefact, a CI step’s logs, or a Slack/Discord webhook that echoes env.
- –A frontend bundle, even server-rendered. The browser sees everything the bundle contains.
- –An order log that includes the signed payload. The signature is a credential; treat it as such.
- –A persistent log file you back up off-host without redaction.
Set production env vars on Railway
# From your project root, after `railway link`:
railway variables --set "LIMITLESS_API_KEY=…"
railway variables --set "PRIVATE_KEY=0x…"
railway variables --set "RPC_URL=https://…"
railway variables --set "PANEL_TOKEN=$(openssl rand -hex 32)"
# Verify
railway variables
Module 01 walks the full deploy. The point here: secrets enter the platform via CLI or web UI, never via a committed file.
Section 05
Signing & idempotency.
Two patterns prevent the most expensive bot bugs: a tight nonce on every signed order so a captured signature can’t be replayed, and an idempotency key on every API call so a network retry doesn’t become a double-spend. Both are five lines of code; both have saved real money.
Nonce + tight expiry on every signature
Every EIP-712 order includes a per-account nonce and an expiry timestamp. Nonce prevents the same signature being used twice; expiry caps how long a captured signature is useful for.
- –Nonce comes from the API (GET /account/nonce); never reuse one.
- –Expiry is short, usually now + 60s. A captured signature past expiry is useless.
- –Never log the signed payload. Log the order shape, not the credential.
Idempotency keys on every order
When your bot retries a request that timed out, the server must know whether the original succeeded. An idempotency key, a per-order UUID sent in a header, lets the server return the same result on the second try instead of placing a duplicate order.
# Every order request:
headers["Idempotency-Key"] = uuid4()
# Retry uses the SAME key, no duplicate fills.
A retry without idempotency is a bug, not a feature.
The most expensive cluster of API trading-bot bugs is “timed out, retried, both succeeded.” Idempotency keys cost five characters; the bug they prevent costs whatever your max-position-size is. Module 09 (Errors) has the canonical retry pattern in code.
Section 06
Webhook signing.
The moment your bot (or your panel) accepts an inbound HTTP request, fill notifications, market events, anything, you need to know it actually came from who it claims. Every webhook Limitless sends is signed; every webhook you accept must verify the signature before doing anything.
import crypto from 'node:crypto';
function verify(rawBody: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(signature, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// In your handler:
// const raw = await req.text(); // raw body, NOT parsed JSON
// const sig = req.headers['x-signature']; // provider's header name
// if (!verify(raw, sig, process.env.WEBHOOK_SECRET!)) return res.status(401).end();
// const payload = JSON.parse(raw);
import hmac
import hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
"""Compare HMAC-SHA256 of the raw body against the provider's signature."""
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# In your FastAPI handler:
# raw = await request.body() # raw bytes, NOT parsed JSON
# sig = request.headers.get("x-signature", "")
# if not verify(raw, sig, os.environ["WEBHOOK_SECRET"]):
# raise HTTPException(401)
# payload = json.loads(raw)
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func Verify(rawBody []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal is constant-time
return hmac.Equal([]byte(expected), []byte(signature))
}
// In your handler:
// raw, _ := io.ReadAll(r.Body)
// sig := r.Header.Get("X-Signature")
// if !Verify(raw, sig, os.Getenv("WEBHOOK_SECRET")) { http.Error(w, "401", 401); return }
Three details that catch most teams.
- –Verify against the raw body. Re-serialising parsed JSON changes whitespace and breaks the signature.
- –Use a timing-safe compare. Plain == leaks the signature one byte at a time.
- –Reject requests with a stale timestamp. Replay protection is part of signing, not separate from it, check the provider’s docs for their freshness window (usually 5 minutes).
Section 07
Rotation & incident.
Two questions every operator should be able to answer in their sleep. What do I rotate, and how often? and Something just leaked, what now? The answers below are the personal-trader floor; team setups should harden further.
Rotation cadence
- –Limitless API key: rotate every 90 days; rotate on any laptop change.
- –RPC keys (Alchemy / Infura): rotate quarterly; rotate immediately on any GitHub-search false alarm.
- –Burner private key: generate a fresh burner whenever the balance hits zero on its own, every quarter regardless, and immediately on incident.
- –Panel token: rotate any time someone leaves a team, or every 90 days for solo setups.
Rotating a key the first time always feels harder than it is. howtorotate.com is an open-source collection of provider-specific rotation tutorials, AWS, GitHub, Alchemy, Stripe, dozens more, that walks you through each one. Bookmark it; you’ll come back when something leaks.
Incident playbook
- Trip the kill switch. The bot stops trading; figure out the rest later.
- Move funds first. If the wallet key is suspect, send the balance to a fresh burner before anything else.
- Cancel open orders. Anything resting in the book with the leaked key’s signature is now an attacker tool.
- Revoke the leaked credential. Don’t try to clean git history. Just rotate, the old value is permanent on someone’s clone somewhere.
- Audit recent activity. Pull the last hour of orders / API calls / panel actions. Confirm what was you and what wasn’t.
- Write it up. Even for a personal incident, a five-line note (“what leaked, how, what I changed”) makes the next one less catastrophic.
The kill switch is the single most important habit.
You will not figure out what happened in five seconds. You can trip the kill switch in five seconds. Module 02 (Trader Control Panel) wires this so a single tap from your phone halts the bot on the next iteration. Build the habit early; treat it the way you treat the brake pedal, not optional, not advanced.
Floor reached
You know enough to start safely.
Threat model internalised, secrets in .env, .gitignore strict, pre-commit hook armed, burner wallet capped, signing + idempotency understood, webhook verification ready, rotation cadence on the calendar. Every module from here on assumes this floor.