Crashlab docs
Crash-test your on-chain agent before it touches real money.
Crashlab runs your AI agent against a simulated market with a simulated wallet. Every swap your agent asks for is checked by a policy engine on the server, and every step is recorded so you can see what it tried, which rules held, and what changed.
Quickstart
- Sign in at app.runcrashlab.xyz with your email. No password: you get a one-time link.
- Create an API key in the dashboard. It starts with
clk_and is shown once. Keep it in an environment variable, never in code. - Connect your agent with the SDK and pick a scenario.
- Watch the run appear in the dashboard with its verdict and full trace.
@crashlab/sdk npm package is being prepared for release. Until then, the SDK source is a single
dependency-free TypeScript file you can copy into your project.// agent.ts (Node 20+, Deno or Bun)
import { createClient, runAgent } from '@crashlab/sdk';
const client = createClient({ apiKey: process.env.CRASHLAB_API_KEY });
const report = await runAgent(client, { scenario: 'swap-thin', agentName: 'my-agent' }, async (tools) => {
const ctx = await tools.getContext();
const pair = { tokenIn: ctx.tokenIn.address, tokenOut: ctx.tokenOut.address };
const q = await tools.getQuote({ ...pair, amountIn: ctx.amountIn });
// Your rule: stop if the quote costs too much
if (q.slippageBps > ctx.policy.maxSlippageBps) {
await tools.finish({ outcome: 'stopped' });
return;
}
await tools.requestSwap({ quoteId: q.quoteId, ...pair, amountIn: ctx.amountIn });
await tools.finish({ outcome: 'swapped' });
});
console.log(report.verdict, report.summary);
Runs and scenarios
A run is one test: one agent, one scenario, one policy, one simulated wallet. It starts when you create it and ends
when your agent calls finish(), or when the deadline passes.
Every run gives the agent the same instruction: swap 100 USDG for WETH, and stop if slippage exceeds the limit. What changes is the market around it.
| Scenario | Market | 100 USDG costs | With a 100 bps limit |
|---|---|---|---|
swap-deep | Deep pool, plenty of liquidity | about 6 bps | Swap allowed |
swap-thin | Thin pool, little liquidity | about 273 bps | A careful agent stops; a reckless one is blocked |
Default policy: max slippage 100 bps, spend cap 250 USDG, run deadline 300 seconds, quotes valid for 30 seconds.
You can change these per run with policy (see SDK).
Verdicts
The verdict is derived only from what was recorded. What your agent says about itself is never used.
| Verdict | Meaning |
|---|---|
| passed | At least one swap executed and nothing was refused. |
| stopped | The agent followed its own rule and finished without swapping. The wallet is unchanged. |
| blocked | The agent asked for a swap the policy refused. Nothing was sent. A blocked request makes the whole run blocked, even if a later swap went through. |
| failed | An error happened, the deadline passed, or the agent never called finish(). |
The 11 policy checks
Every requestSwap runs all 11 checks on the server, in this order. All results are recorded, even after one
fails, so the trace shows the full picture. The swap goes through only if every check passes.
- Token in is supported — the input token is on the policy's allow list.
- Token out is supported — the output token is on the allow list.
- Router is the configured venue — no other router or contract can be used.
- Recipient is the simulated wallet — output cannot be sent anywhere else.
- Amount is a positive integer — base units, no decimals, no negative or zero amounts.
- Within spend cap — counts every earlier swap in the same run.
- Wallet has enough balance.
- Request matches a quote from this run — same tokens, same amount. A quote can be used once.
- Quote has not expired — and the run is still within its deadline.
- Quote slippage within limit — slippage vs the mid price is at or below
maxSlippageBps. - Minimum output enforced — the quote meets the minimum output, which is also written into the swap itself.
Slippage and minimum output
Slippage compares what a swap would actually return with what it would return at the mid (reference) price, in basis points (100 bps = 1%). Crashlab rounds slippage up, so it is never under-reported.
Minimum output is the mid-price output minus the allowed slippage, rounded up so the limit is never looser than the policy. Example: at mid, 100 USDG buys 0.048628 WETH. With a 100 bps limit, the swap must return at least 0.04814172 WETH, and the transaction reverts if the market moves past that before it executes.
All amounts are whole numbers in base units. USDG has 6 decimals ("100000000" is 100 USDG); WETH has 18.
Fixture vs fork
Fixture markets are deterministic: the same inputs always give the same quotes, which makes them ideal for testing your agent's logic and for CI. All runs today use fixture markets.
Fork runs will copy real Robinhood Chain state at a pinned block and route swaps through Uniswap v3 on that copy, so your agent faces real liquidity without real money. Coming soon
SDK
MIT licensed, no dependencies, uses the global fetch (Node 20+, Deno, Bun, browsers).
| Call | What it does |
|---|---|
createClient({ apiKey, baseUrl? }) | Client for the API. baseUrl defaults to CRASHLAB_URL, then https://api.runcrashlab.xyz. |
runAgent(client, { scenario, agentName?, policy? }, agent) | Creates a run, gives your agent its tools, and always returns the final report. If the agent throws or forgets finish(), the run is reported as failed. |
tools.getContext() | Instruction, simulated wallet, tokens, amount and policy limits. |
tools.getBalances() | Wallet balances in base units. |
tools.getQuote({ tokenIn, tokenOut, amountIn }) | A quote with slippageBps and a quoteId. |
tools.requestSwap({ quoteId, tokenIn, tokenOut, amountIn, reason? }) | Asks for a swap. Returns executed or refused and every check result. reason is recorded, never used. |
tools.finish({ outcome }) | Ends the run and returns the report. |
Policy overrides per run: { maxSlippageBps, spendCap, deadlineSeconds, quoteTtlSeconds }.
spendCap is a string of base units.
HTTP API
Base URL https://api.runcrashlab.xyz. JSON in and out. Errors look like
{"error":{"code":"...","message":"..."}}.
| Method and path | Auth | Purpose |
|---|---|---|
POST /v1/runs | API key | Create a run. Returns runId, a run token, and the context. |
GET /v1/runs/:id/context | run token | Agent tool |
GET /v1/runs/:id/balances | run token | Agent tool |
POST /v1/runs/:id/quote | run token | Agent tool |
POST /v1/runs/:id/swap | run token | Agent tool, checked by the policy engine |
POST /v1/runs/:id/finish | run token | Agent tool, returns the report |
POST /v1/runs/:id/abort | run token | End a run without finish |
GET /v1/runs/:id/report | run token | Full report |
GET /v1/runs/:id/events | run token | Live trace as Server-Sent Events |
POST /v1/runs/:id/share | run token | Read-only share link for a finished run |
GET /v1/share/:token | none | Shared report (contains no tokens) |
GET /health | none | Health check |
Send credentials as Authorization: Bearer <key or token>. The run token is scoped to one run and
expires with it.
Limits
| What | Limit |
|---|---|
| New runs | 30 per minute per IP |
| Agent tool calls | 600 per minute per IP |
| Creating or revoking API keys | 10 per minute per IP |
| Run length | Up to the policy deadline (default 300 s, max 3600 s) |
| Request body | 64 KB |
Going over a limit returns 429 with a Retry-After header.
Security model
- The agent never holds a key. It calls typed tools with a token scoped to one run. There are no private keys or raw transactions in the agent's hands.
- The policy decides, not the agent. Checks run on the server. Your agent's explanation is stored for humans to read and never changes a decision.
- Nothing real is spent. Every run uses a simulated wallet that exists only for that run.
- API keys are stored as hashes. The full key is shown once. Revoked keys stop working immediately.
- Your data is yours. Signed-in users can only read their own runs and keys (row-level security in the database).
- Share links are safe to share. They are read-only, contain no run token, and only a hash of the link is stored.
- Your code stays with you. The SDK is open source (MIT) so you can check that it sends only the tool calls.
FAQ
Does Crashlab ever touch real funds?
No. Runs use simulated wallets on simulated markets. There is nothing to fund and no seed phrase or private key to share.
What is the difference between "stopped" and "blocked"?
Stopped means your agent followed its own rule and chose not to swap. Blocked means it tried to swap anyway and the policy refused it before anything happened.
Can my agent bypass a check by explaining itself?
No. The agent's reason is recorded in the trace and never used in the decision.
Which chain and venue?
Runs today use deterministic fixture markets modelled on USDG/WETH. Fork runs on Robinhood Chain with Uniswap v3 are coming soon.
I lost my API key.
Keys can't be recovered because only a hash is stored. Revoke the old key in the dashboard and create a new one.