A tiny Bun + Hono service that shells out to forge test -vvvv --color always and streams the raw stdout/stderr back as JSON. The bundled HTML helper (run-tests.html) renders Forge's ANSI-colored output and can fire multiple simulated requests to demonstrate the server-side single-flight queue.
- Async trace queue -
/api/queueaccepts trace requests, returns immediately with 202, and POSTs results to your callback URL when complete. - API key authentication - incoming requests are authenticated via
X-API-Keyheader; callbacks include authentication for your webhook to verify. - ✅ Single-run queue – every
/api/run-testscall waits for the previous Forge run to finish, so two users can't mutate the project simultaneously. - ✅ Forge environment overrides – optional JSON body (
from,to,value,rpc,calldata,previous_tx) is forwarded as uppercase env vars before each run. - ✅ Color-preserving output – the API forces Forge to emit ANSI colors, and the demo UI renders them so traces look exactly like the terminal.
- ✅ Docker-first – multi-stage image installs Foundry once in a builder layer and copies only the runtime bits into
oven/bun:1-slim(~! significantly smaller than the original 777 MB image).
bun install
foundryup # ensures ~/.foundry/bin/forge exists
bun run index.tsThe server prints which forge binary/project directory it found and starts listening on PORT (default 3000).
| Method | Path | Description |
|---|---|---|
| GET | / |
Hello world text response (basic liveness probe). |
| GET | /api/health |
{ "status": "ok" }. |
| POST | /api/run-tests |
(Legacy) Synchronous test execution. Runs forge test --color always -vvvv in FORGE_PROJECT_DIR. Responses look like { success, exitCode, stdout, stderr }. Non-zero exit codes return HTTP 500. |
| POST | /api/queue |
(Recommended) Queue a trace request for async processing. Returns immediately with 202 Accepted. Results are POSTed to the provided callback URL. |
Queue a trace request for async processing. The server immediately returns 202 Accepted and processes the trace asynchronously, sending results to the provided callback URL.
Headers:
Content-Type: application/jsonX-API-Key: {your-api-key}- Required for authentication (must match one ofDAPP_API_KEYS)
Request Body:
{
"rpc_url": "https://rpc.example.com",
"callback_url": "https://your-app.com/webhook?trace_id=123",
"chain_id": 1,
"transaction_hash": "0x...",
"transaction": {
"from": "0x...",
"to": "0x...",
"value": "0",
"data": "0x..."
},
"previous_transactions": [],
"block_env": {
"number": "12345678",
"timestamp": "1704672000",
"beneficiary": "0x...",
"gas_limit": "30000000",
"basefee": "1000000000",
"difficulty": "0",
"prevrandao": null,
"blob_excess_gas_and_price": null
}
}Required Fields:
rpc_url- RPC URL for the target chaincallback_url- URL where trace results will be POSTedtransaction_hash- Transaction hash being tracedtransaction.from- Sender addresstransaction.value- Value in wei
Optional Fields:
chain_id- Chain ID for logging/contexttransaction.to- Target address (empty for contract creation)transaction.data- Calldata (hex encoded)transaction.nonce- Transaction noncetransaction.gas_limit- Gas limitprevious_transactions- Array of previous transactions for multi-block simulationblock_env- Block environment for accurate simulation
Response (202 Accepted):
{
"status": "queued",
"message": "Trace job queued successfully"
}Callback Payload:
When trace completes, a POST request is sent to callback_url with:
{
"success": true,
"trace_content": "<ANSI-colored trace output>",
"trace_format": "ansi",
"duration_ms": 1234
}Or on failure:
{
"success": false,
"error": "Error message",
"error_code": "TRACE_FAILED",
"duration_ms": 1234
}The callback includes an X-API-Key header with TRACER_CALLBACK_API_KEY for your server to verify authenticity.
Important behaviors:
trace_contentcontains only the target transaction trace - Setup traces, previous transaction traces, and test summaries are filtered out. You receive only the call tree for the transaction you requested.- Reverting previous transactions don't halt the trace - When
previous_transactionsincludes transactions that revert, the tracer continues processing to accurately reconstruct blockchain state. This is essential for correct tracing since reverts are part of block history.
curl Example:
curl -X POST http://localhost:3000/api/queue \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your-api-key' \
-d '{
"rpc_url": "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
"callback_url": "https://your-app.com/webhook",
"chain_id": 1,
"transaction_hash": "0x1234567890abcdef...",
"transaction": {
"from": "0xSenderAddress...",
"to": "0xContractAddress...",
"value": "0",
"data": "0xCalldata..."
}
}'The /api/queue endpoint requires API key authentication via the X-API-Key header.
All requests to /api/queue must include a valid API key in the X-API-Key header. Valid keys are configured via the DAPP_API_KEYS environment variable (comma-separated list).
Responses:
401 Unauthorized- Missing or invalid API key500 Internal Server Error- Server misconfigured (no API keys set)
When the tracer sends results to your callback_url, it includes an X-API-Key header containing the value of TRACER_CALLBACK_API_KEY. Your webhook endpoint should verify this header to ensure the request came from the tracer.
Example webhook verification (pseudocode):
app.post('/webhook', (req, res) => {
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.EXPECTED_TRACER_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Process trace results...
});Every field is optional – unset values are ignored.
| JSON key | Forwarded env var | Purpose |
|---|---|---|
from |
FROM |
Sender address for the traced call. |
to |
TO |
Contract to call. |
value |
VALUE |
Call value (wei/ether/etc). |
rpc |
RPC |
RPC URL (used by the forge tests). |
calldata |
CALLDATA |
Hex calldata forwarded to the test harness. |
previous_tx |
PREVIOUS_TX |
Optional identifier to chain traces together. |
Example request that reuses any matching shell env vars:
curl -sS -X POST http://localhost:3000/api/run-tests \
-H 'Content-Type: application/json' \
-d "$(jq -n \
--arg from "${FROM:-}" \
--arg to "${TO:-}" \
--arg value "${VALUE:-}" \
--arg rpc "${RPC:-}" \
--arg calldata "${CALLDATA:-}" \
--arg previous_tx "${PREVIOUS_TX:-}" \
'$ARGS.named | with_entries(select(.value != ""))')"success: boolean convenience flag (exitCode === 0).exitCode: Forge's exit status.stdout/stderr: raw output strings (with ANSI escapes preserved).
Because the server serializes runs, concurrent callers will see one request block until the previous finishes.
Open the file directly in a browser or serve it with any static file server. When opened from the filesystem it automatically points at http://localhost:3000; when served from the Bun API origin it reuses the current host. Features:
- Manual Run button that mirrors the curl call.
- Optional form inputs for all supported env overrides.
- Burst simulator that fires N requests simultaneously so you can watch the queue in action – result cards show pass/fail, duration, and a colorized stdout snippet.
The repository ships with a multi-stage Dockerfile:
docker build -t foundry-tracer-api .
docker run --rm -p 3000:3000 foundry-tracer-apibuilderstage (oven/bun:1) installs apt utilities, Foundry, and Bun dependencies.runtimestage (oven/bun:1-slim) copies/appand/home/bun/.foundry, sets ownership to thebunuser, and exposes port 3000.
This keeps the final image lean while still bundling Foundry so the container works out of the box.
Environment variables you may care about:
| Variable | Default | Notes |
|---|---|---|
PORT |
3000 |
HTTP port for Bun. |
FORGE_PROJECT_DIR |
<repo>/foundry (or /app/foundry) |
Directory passed to forge test. |
FORGE_BIN |
auto-resolved | Hard-set if forge lives elsewhere. |
FOUNDRY_HOME |
$HOME or /tmp/foundry |
Used when building Foundry env vars. |
DAPP_API_KEYS |
- | Comma-separated list of valid API keys for /api/queue authentication. Required for /api/queue to work. |
TRACER_CALLBACK_API_KEY |
- | API key sent in X-API-Key header when posting results to callback URLs. Helps callback receivers verify authenticity. |
The server also injects FORCE_COLOR=1, CLICOLOR=1, and CLICOLOR_FORCE=1 to keep colored logs when piping through Bun.
For a complete walkthrough using a real Linea mainnet transaction, see the Real-World Testing Example. It demonstrates:
- Using
nc(netcat) to simulate your dapp's callback endpoint - How
trace_contentreturns only the target transaction trace - How reverting previous transactions don't halt the trace job
Quick start:
# Terminal 1: Start callback listener (simulates your dapp's webhook)
nc -l 8080
# Terminal 2: Start tracer
DAPP_API_KEYS=test-api-key bun --hot index.ts
# Terminal 3: Send request (see docs/local-testing-example.md for full curl)- Use
bun run index.tslocally, then hitrun-tests.htmlfor a nicer visualization. - The queue is simple but effective: every request is awaiting the previous run promise. If you ever need parallelism, remove or expand it deliberately.
curl -Nor tools likegrcaren't required – the response already carries ANSI escapes for rich rendering on the client side.
Happy tracing!