Welcome to API Academy
Module 01 · Foundations · ~25 min
Infrastructure.
By the end of this module, you’ll have a real address on the internet that a trading bot can call home, somewhere it can run, restart, and keep its memory without your laptop staying open.
Before any bot code, before any control panel, you need a service running on the internet with a URL you can hit from your phone. This module ships a hello-world service to Railway, sets the env vars and the persistent volume the rest of the curriculum will use, and ends with you opening the URL on your phone. Twenty-five minutes; nothing yet trades, but everything from here on assumes this floor.
How do you deploy a trading bot to Railway?
Five commands from the Railway CLI: npm i -g @railway/cli (or brew install railway), railway login, railway init, railway up to build and deploy, then railway domain to mint a public URL you can open on your phone. The service itself is one file with two endpoints: / returns JSON and /health returns 200 for Railway’s health check; bind to 0.0.0.0, read PORT from the environment, and let a one-line Procfile tell Railway how to start it. Set secrets like LIMITLESS_API_KEY with railway variables --set (never in the repo), and mount a 1 GB volume at /app/data, addressed via ACADEMY_DATA_DIR, so SQLite files and NDJSON logs survive every redeploy. Nothing trades yet, but every later module assumes this floor: a URL, production env vars, persistent storage, and tailable logs.
Platform commands and pricing are illustrative.
Section 01
Why deploy first.
The classic order, build the bot on your laptop, then figure out hosting at the end, sounds reasonable and ages badly. By the time the bot works locally, the platform decisions, the env-var habits, and the persistent-storage path are all things you’re trying to retrofit around code that already assumes localhost. The result is a panicky last-mile sprint with real money on the table.
Deploying empty is the cheapest way to learn the platform. A hello-world that returns a JSON object teaches the same Procfile, the same env vars, the same volume mount, and the same domain wiring as a full bot, and if you break it, you’ve broken five lines of HTTP server code, not your trading loop. By the end of this module you have infrastructure; in Module 02 you put a control panel on it that surfaces positions, P&L, and a kill switch; in Modules 03–18 every section ends with “wire this into your panel.”
What “the floor” means in this curriculum
- A URL. Reachable from your phone, your laptop, and a coach if you have one. Not localhost.
- Production env vars. Set by CLI or UI, never in the repo. The deployed process reads them on boot.
- Persistent storage. A path that survives redeploys, for SQLite, for the NDJSON log, for any state the bot needs.
- Tailable logs. A single command tails the running process. You can read stdout from anywhere with internet.
Section 02
Pick your platform.
The curriculum picks Railway as the default because it gets you to a production URL with persistent storage in about ten minutes, with a free trial that’s long enough to finish the first three modules. The patterns, Procfile, env vars, volume mount, domain, carry over to Fly, Render, or a $5 VPS. Pick whichever matches how you want to learn; the rest of this module uses Railway commands.
Railway
Curriculum defaultWhy: CLI-first deploy, generous free trial, built-in persistent volumes, env-var UI + CLI, log streaming, custom domains in one command, nixpacks autodetects Python, Node, and Go.
Watch: trial ends; budget around $5–$10/mo for the panel + bot combined past that.
Fly.io
Why: region-aware deploy, slightly cheaper at idle, similar Procfile-style pattern via fly.toml.
Watch: persistent volumes are per-region, so multi-region is more involved than Railway.
Render
Why: friendly UI, good free tier for static + small services, native cron.
Watch: free tier sleeps; not ideal for a bot that needs to wake on a schedule.
$5 VPS
Why: Hetzner / DO / Vultr give you a flat $5–$6/mo bill and a real Linux box. Best long-term economics.
Watch: you’re the platform, systemd, certbot, ufw, log rotation. Worth it once you’ve done the managed path first.
Section 03
The hello-world service.
One file, two endpoints. / returns a JSON object so you have something to look at on your phone; /health returns 200 so Railway’s health-check passes. All three runtimes share the same Procfile pattern. Pick the tab that matches what you installed in Setup.
// app.ts, minimal hello-world for Railway
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
app.get('/', (c) => {
return c.json({
service: 'api-academy-deploy',
module: 'Module 01, Infrastructure',
msg: 'You are reading this from a server you deployed.',
ts: new Date().toISOString(),
});
});
app.get('/health', (c) => c.text('ok'));
const port = Number(process.env.PORT) || 8080;
serve({ fetch: app.fetch, port });
console.log(`listening on :${port}`);
# app.py, minimal hello-world for Railway
import os
from datetime import datetime, timezone
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {
"service": "api-academy-deploy",
"module": "Module 01, Infrastructure",
"msg": "You are reading this from a server you deployed.",
"ts": datetime.now(timezone.utc).isoformat(),
}
@app.get("/health")
def health():
return "ok"
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 8080))
uvicorn.run(app, host="0.0.0.0", port=port)
// main.go, minimal hello-world for Railway (stdlib only)
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"service": "api-academy-deploy",
"module": "Module 01, Infrastructure",
"msg": "You are reading this from a server you deployed.",
"ts": time.Now().UTC().Format(time.RFC3339),
})
})
http.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "ok")
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Procfile
One line. Railway reads Procfile to know how to start the service. The same Procfile pattern works on Heroku, Fly, and a VPS via honcho.
# Procfile (TypeScript)
web: node --env-file=.env app.ts
# Procfile (Python)
web: uvicorn app:app --host 0.0.0.0 --port $PORT
# Procfile (Go), build first, then run the binary
# release: go build -o api-academy-deploy ./...
web: ./api-academy-deploy
requirements / package files
Pinned versions. Railway’s nixpacks builder reads these to install dependencies. Don’t skip the pin, floating versions are how supply-chain compromises slip in.
# requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.30.6
# package.json (TS), npm i hono @hono/node-server
{ "dependencies": {
"hono": "4.6.5",
"@hono/node-server": "1.13.2"
} }
# go.mod (Go), stdlib only, no third-party deps
module api-academy-deploy
go 1.22
Section 04
Deploy it.
Railway’s CLI is the fastest path to a URL. Five commands, in order. The first one is interactive (a browser tab opens), everything after that runs unattended.
If railway login hangs
In a sandboxed terminal, the browser tab won’t open automatically. Use railway login --browserless, it prints a URL and a code to paste.
From your project root
# 1. Install + log in
npm i -g @railway/cli # or: brew install railway
railway login # opens a browser tab
# 2. Create or link a project
railway init # name it "api-academy-deploy"
# 3. Deploy
railway up # uploads, builds, runs
# 4. Set env vars (one per line, repeat as needed)
railway variables --set "LIMITLESS_API_KEY=lmt_…"
railway variables --set "PANEL_TOKEN=$(openssl rand -hex 32)"
# 5. Generate a public domain
railway domain # prints something like api-academy-deploy.up.railway.app
After step 5, hit the URL in your browser. You should see the JSON from /. If you don’t, the next section’s log-tailing command will tell you why.
Section 05
Persistent storage.
Every redeploy gives you a fresh container with a fresh filesystem. Anything written to disk during one deploy is gone after the next push. The fix is a volume, a chunk of storage that survives redeploys, mounted at a known path. Module 02 puts the panel’s seed data here; Module 13 (PnL Analysis) puts the SQLite-backed order history here; Module 18 (Production Bot) puts the NDJSON event log here. Wire it once, in Module 01.
Mount a volume at /app/data
# From the Railway dashboard, Service → Volumes → New
# Mount path: /app/data
# Size: 1 GB (more than enough for the curriculum)
# Or via CLI (newer versions):
railway volume add --mount-path /app/data --size 1
# Tell your code where to write. Set this once and use it everywhere.
railway variables --set "ACADEMY_DATA_DIR=/app/data"
# Verify the volume survives a redeploy:
railway run "echo first-deploy > /app/data/touch.txt"
railway up # redeploy
railway run "cat /app/data/touch.txt" # should print: first-deploy
Reading the path in code
# Python
import os
DATA_DIR = os.environ.get("ACADEMY_DATA_DIR", "./data")
LOG_PATH = os.path.join(DATA_DIR, "bot.log.ndjson")
# TypeScript
const DATA_DIR =
process.env.ACADEMY_DATA_DIR ?? './data';
const LOG_PATH =
`${DATA_DIR}/bot.log.ndjson`;
// Go
dataDir := os.Getenv("ACADEMY_DATA_DIR")
if dataDir == "" { dataDir = "./data" }
logPath := filepath.Join(dataDir, "bot.log.ndjson")
The fallback to ./data means the same code runs locally without the env var set. Module 02 follows this pattern for the panel’s seed data.
Don’t put these on the volume
- –Secrets. Env vars are encrypted; volumes are not. Keys go in env, never on disk.
- –Anything you can rebuild from source. Build artefacts, vendored deps, the volume is for state, not code.
- –Long-term audit logs. 1 GB fills fast under verbose logging. Rotate or stream off-host.
Section 06
Open it on your phone.
This is the part that matters. The hello-world is doing nothing impressive, it returns a JSON object, but it’s doing it from a server you deployed, behind a domain you bound, reachable from a device you didn’t configure. The “a thing I made is on the actual internet” moment is the entire point of moving infrastructure first.
Pull out your phone. Type the domain Railway printed. You should see the JSON. If you don’t, the log will tell you why, the next code block tails the running process so you can read stdout from anywhere.
Tail logs from anywhere
# Live tail, Ctrl+C to exit
railway logs
# Last 100 lines (no follow)
railway logs --tail 100
Mockup, yours will show your timestamp.
Bot infrastructure on Railway: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
Why deploy infrastructure before writing the trading bot?
Because retrofitting hosting around code that assumeslocalhostages badly: by the time the bot works locally, you’re bolting platform decisions, env-var habits, and storage paths onto a working trading loop with real money on the table. Deploying an empty hello-world teaches the same Procfile, env vars, volume mount, and domain wiring as a full bot, and if you break it, you’ve broken five lines of HTTP server code, not your strategy. -
What is a Railway volume and where should a bot mount it?
A volume is storage that survives redeploys; without one, every deploy gives the container a fresh filesystem and wipes anything written to disk. The curriculum mounts 1 GB at/app/data(viarailway volume add --mount-path /app/data --size 1) and setsACADEMY_DATA_DIR=/app/dataso code reads the path from env, falling back to./datalocally. Later modules put the panel’s seed data, the SQLite-backed order history, and the NDJSON event log there. -
Where do a trading bot’s API keys and secrets live?
In environment variables set viarailway variables --set, never in the repo, never in the image, never in a log. Env vars are encrypted; volumes are not, so keys never go on disk. Generate tokens likePANEL_TOKENwithopenssl rand -hex 32and confirm they show up inrailway variables. The deployed process reads them on boot. -
Is Railway the only option for hosting a trading bot?
No. Railway is the curriculum default because it reaches a production URL with persistent storage in about ten minutes, but the patterns (Procfile, env vars, volume mount, domain) carry over to Fly.io, Render, or a $5 VPS. Watch the costs: Railway’s trial ends, so budget around $5–$10/mo for the panel plus bot; Hetzner, DO, or Vultr give a flat $5–$6/mo Linux box once you’re ready to be the platform yourself. -
How do you read your bot’s logs on Railway?
railway logslive-tails the running process from any machine with internet (Ctrl+C to exit);railway logs --tail 100prints the last 100 lines without following. Tailable logs are part of the module’s definition of “the floor”: if the URL doesn’t show your JSON after a deploy, the log tells you why.
Section 07
Module checklist.
Every box checked means you have running infrastructure. Module 02 will deploy a control panel on top of it; if any of these fail, fix them now, everything downstream assumes the floor.
Hello-world service runs locally on localhost:8080 and returns JSON at /.
A Procfile exists and the start command binds to 0.0.0.0 on $PORT.
Railway CLI installed; railway login + railway init done.
First deploy via railway up succeeded; the build logs show no errors.
At least one env var (e.g. PANEL_TOKEN) set via railway variables --set and visible in railway variables.
Persistent volume mounted at /app/data; ACADEMY_DATA_DIR set; survives a redeploy.
Production URL generated via railway domain and bookmarked.
Opened the URL on your phone; saw the JSON; tailed logs once via railway logs.
Module 01 complete
On the air.
Your bot has an address now. When it eventually places trades, it has somewhere to live that doesn’t disappear when you close your laptop, and a memory that survives every redeploy.
Concretely, you shipped a service to the internet, set production env vars without committing them, mounted a volume that survives redeploys, and hit your domain from a device you didn’t configure. The hello-world is a placeholder; the floor underneath it is the real artefact.
A URL beats localhost for every kind of feedback, debugging, sharing, intervening from your phone. Deploy first, build second.
Secrets live in env vars, set via the platform’s CLI or UI. Never in the repo, never in the image, never in a log.
Persistent state has one home: a volume mounted at /app/data, addressed via ACADEMY_DATA_DIR. Every later module assumes both.
Next up: Module 02 builds the Trader Control Panel on top of this infrastructure, a single-page surface for positions, P&L, and a kill switch, running on seed data so you see your first “trade” on the panel before the bot even exists. The visceral moment.
Complete the checklist above to unlock