Before any code touches money

Security guide · ~15 min read · bookmarkable

Security.

An agent that places trades is a piece of software you trust with your money. Before you write a line of agent code, internalise the threat model and the seven hygiene rules that keep a leak from becoming a loss. Bookmark this page, you’ll come back to it the day something does leak.

I’ve read this. Continue to Module 01

What this page is and isn’t

Is: the floor for everyone who builds an agent that touches a wallet or an API key. Read it once before Module 01, revisit it 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 agent 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 agent losing money. Each has a primary defence later in this page, and a secondary defence elsewhere in the curriculum. None of them require a state-actor adversary, they happen routinely to careful people who skip one step.

Threat 01 Key leak via git

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.

Threat 02 Wallet drain via private key

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 the kill-switch pattern in Module 14.

Threat 03 Prompt injection

A malicious string in market metadata, a webhook payload, or a tool result convinces the LLM to ignore its system prompt and call place_limit_order on a market you didn’t intend. Defence: never put secrets in the prompt (Section 05); Module 15 goes deep.

Threat 04 Runaway loop

A bug or a model glitch causes the agent to call its tools in a tight loop, burning LLM credits and placing repeated orders. Defence: per-iteration spend caps + iteration limits (Module 14), a dashboard that shows you in real time (Module 02).

Threat 05 Supply-chain compromise

An npm or PyPI package you depend on is hijacked and ships malicious code that reads your environment variables. Defence: pin versions, audit dependencies, never curl | sh install scripts, and keep the deploy environment minimal.

Threat 06 Operator-panel takeover

Your dashboard URL is reachable from the public internet without authentication. Anyone who finds it can trip your kill switch, or worse, send a manual override. 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 prompt, never in a log line. 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; it’s the same keys with dummy values so a teammate (or future-you) knows what variables the project expects.

# .env.example  (commit this)
ANTHROPIC_API_KEY=sk-ant-REPLACE
LIMITLESS_API_KEY=REPLACE_WITH_TOKEN_ID
PRIVATE_KEY=0xREPLACE
PANEL_TOKEN=REPLACE_WITH_RANDOM

Strict .gitignore

# Secrets
.env
.env.*
!.env.example
*.pem
*.key
private_key.txt

# Trace artefacts (can leak tool outputs)
agent.log.ndjson
traces/
conversations/

# Local dev databases that hold tokens
*.sqlite
*.db

The ! prefix re-includes .env.example after the wildcard ignores everything starting with .env.

Pre-commit hook, fail loud if a secret slips through

Drop this in .git/hooks/pre-commit and chmod +x it. It blocks any commit that contains common secret prefixes anywhere in the staged diff.

#!/usr/bin/env bash
# Block obvious secret leaks. Not a substitute for hygiene; a backstop.
PATTERNS='sk-ant-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{20,}|0x[a-fA-F0-9]{64}'
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 an agent 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.

1

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.

2

Smallest workable balance

Fund with the smallest amount that lets the agent 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.

3

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 agent 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 agent 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 LLM prompt, a tool description, or a tool result. The model can be coerced into echoing them back.
  • A persistent log file you back up off-host.

Set production env vars on Railway

# From your project root, after `railway link`:
railway variables --set "ANTHROPIC_API_KEY=sk-ant-…"
railway variables --set "LIMITLESS_API_KEY=…"
railway variables --set "PRIVATE_KEY=0x…"
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 the web UI, never via a committed file.

Section 05

The prompt surface.

Anything the LLM can read, the LLM can be tricked into echoing back. That makes everything you put into a prompt a security boundary, not just user input. Three rules keep the surface clean.

Secrets are tool-only

The LLM never sees an API key. It calls a tool named place_limit_order; the tool reads the key from the environment and signs the request. The model knows the tool exists; it doesn’t know the credential.

Untrusted text is quoted

Any string from a market, webhook, or external feed is wrapped in unambiguous delimiters before it enters the prompt, and the system message tells the model never to follow instructions inside the delimiters. Module 15 has the canonical pattern.

Tool results are scrubbed

Before a tool result goes back to the model, redact anything that looks like a key, an address, or a path. A leaked secret in a tool result becomes a leaked secret in the model’s context, and from there into any log it writes.

Logs are part of the prompt surface.

Anything you write to agent.log.ndjson can leak via a tail-the-log dashboard or a backup. Strip secrets before logging; log the shape of a tool call (“placed order on market X”) not its arguments verbatim. Module 12 covers structured logging.

Section 06

Webhook signing.

The moment your agent (or your dashboard) accepts an inbound HTTP request, 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. The pattern is the same regardless of provider.

import crypto from 'node:crypto';

// secret = a long random string set in BOTH the sender and your env vars
function verify(rawBody: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  // timingSafeEqual prevents leaking the signature byte-by-byte
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(signature, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// In your FastAPI / Express 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);

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 (timingSafeEqual / compare_digest). 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

  • LLM keys: rotate every 90 days, or immediately if you ever paste it anywhere unfamiliar.
  • Limitless API key: rotate every 90 days; rotate on any laptop change.
  • 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, OpenAI, Slack, dozens more, that walks you through each one. Bookmark it; you’ll come back when something leaks.

Incident playbook

  1. Trip the kill switch. The agent stops trading; figure out the rest later.
  2. Move funds first. If the wallet key is suspect, send the balance to a fresh burner before anything else.
  3. Revoke the leaked credential. Don’t try to clean git history. Just rotate, the old value is permanent on someone’s clone somewhere.
  4. Audit recent activity. Pull the last hour of orders / API calls / panel actions. Confirm what was you and what wasn’t.
  5. 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 14 wires this so a single tap from your phone halts the agent 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, prompt surface understood, webhook verification ready, rotation cadence on the calendar. Every module from here on assumes this floor.