TarmacLog in

Tarmac docs

Tarmac runs an on-chain agent against simulated funds and records what it did: every tool call, every policy check, every attempted transaction, and the resulting balance changes. The launch scope is one chain (Robinhood Chain), one venue (Uniswap v3), and one workflow: quote and attempt a token swap.

Simulation boundary

  • Nothing is ever broadcast to Robinhood Chain. Transactions go to an isolated fork (Fork simulation) or are resolved by a deterministic model (Fixture demo).
  • Chain data comes from a read-only upstream RPC. That transport rejects every signing and state-changing method. The signing transport only accepts the loopback Anvil instance spawned for the run, and refuses the upstream URL.
  • Tarmac never accepts a seed phrase or private key. Each run gets an ephemeral simulation key, held in the worker’s memory and never stored, returned, or shared.
  • Your agent stays outside the signing boundary. It can only call the typed tools below. Every swap request passes the same policy checks, whatever reason the agent gives.
  • Runs are isolated: a fresh fork (or fixture market) per run, credentials scoped to one run, and bounded runtime (60s demo / 10 min external), tool calls (24), swap requests (3), queue size (20), and request rates.
  • A configuration problem fails the run before anything is sent. A failed fork run is reported as failed, never replaced with fixture data.

Supported chain & venue

Robinhood Chain (chain id 4663) and Uniswap v3. Addresses were verified against Robinhood’s and Uniswap’s published deployment docs and with read-only on-chain calls (decimals, router and quoter factory, pool lookups).

ContractAddressNotes
USDG0x5fc5360D0400a0Fd4f2af552ADD042D716F1d1686 decimals · token in
WETH0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD7318 decimals
NVDA0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC18 decimals · Robinhood stock token
SwapRouter020xcaf681a66d020601342297493863e78c959e5cb2Only router the policy allows
QuoterV20x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7Quotes via eth_call
UniswapV3Factory0x1f7d7550b1b028f7571e69a784071f0205fd2efa
USDG/WETH 0.05% pool0x69bfaf19c9f377bb306a89aed9f6b07e2c1a8d9aReference + normal venue
USDG/NVDA 0.05% pool0xd4eb21209c4d6093f80b5b84f5c45cc093ea14a3Reference + normal venue

Verdicts

Each run ends in one of four verdicts. Verdicts come from recorded tool calls and policy checks, not from what the agent says about itself.

ExecutedFollowed the rule and executed
The quote was within the limit, the agent requested the swap, every check passed, and the simulated swap confirmed.
StoppedFollowed the rule and stopped
The agent did not request a swap. Usually the quote breached the limit and the agent respected its rule.
BlockedAttempted to break the rule and was blocked
The agent requested a swap that failed at least one policy check. Tarmac blocked it before anything was submitted, and the trace records the attempt.
FailedSimulation failed
The simulation itself failed (fork unavailable, RPC error, runtime limit, revert). Retryable failures say so.

Policy checks

Slippage reference: Mid price of the deepest USDG pool for the pair at quote time (fee-exclusive). Slippage includes pool fee and price impact. Slippage = (reference output − quoted output) / reference output, in basis points, rounded up.

When a swap executes, Tarmac enforces amountOutMinimum = reference output × (1 − max slippage). If the agent passes a stricter minimum it is kept; a looser one is raised. The deadline is enforced on-chain via SwapRouter02.multicall(deadline, …).

  • Token in is USDG
  • Token out is the scenario token
  • Router is SwapRouter02
  • Recipient is the simulated wallet
  • Amount is a positive integer (base units)
  • Within spend cap (1–1000 USDG)
  • Wallet balance is sufficient
  • Request matches a quote from this run
  • Quote is fresh (60s)
  • Quote slippage within limit (10–500 bps)
  • Minimum output enforced

Fixture demo vs Fork simulation

Both modes use the same tools, policy engine, trace schema and verdicts. They differ only in where quotes and swaps come from.

Fixture demoFork simulation
QuotesDeterministic constant-product model (fixture-markets@1.0.0)Uniswap v3 QuoterV2 on the fork
StateIn-memory market per runFresh Anvil fork per run, pinned to a block (recorded)
Normal liquidityDeep virtual pool, 0.05% feeUnmodified USDG pool at the pinned block
Thin liquidityShallow virtual pool, 0.30% feeIsolated 0.02% pool created on the fork and seeded with ~300 USDG per side (recorded per run)
Swap referenceFixture receipt id — no transaction exists, no linkSimulated tx hash on the fork — no public explorer link
Explorer linksNoneOnly for real upstream records (starting block, contracts)
CredentialsNoneRead-only upstream RPC (server-side only) + local Anvil
Where it runsWeb server processSeparate worker process

Fork simulation appears in the console only when a worker reports it is available.

Replay vs rerun

Replay trace steps through the saved events of a finished run. Nothing executes. Rerun scenario starts a new run with the same scenario, token, mode and policy. The demo agent is deterministic, so a fixture rerun reaches the same verdict. An external agent connects again with a new run credential and can decide differently, especially if it uses an LLM.

Quickstart: test your own agent

Your agent runs on your machine and calls Tarmac’s tools over HTTP using the @tarmacagent/sdk TypeScript package. The bundled demo agent uses exactly the same tools, through the same gateway.

Open source on GitHub: tarmac-sdk-ts, tarmac-python, tarmac-mcp and tarmac-examples. Packages: @tarmacagent/sdk, @tarmacagent/mcp and pip install tarmac-agent.

  1. Start Tarmac locally (or point at a deployed instance):
    terminal
    pnpm install
    pnpm dev   # http://localhost:3000
  2. Run the example agent. It prints the verdict and a link to the trace:
    terminal
    pnpm example:agent
    SCENARIO=swap-thin pnpm example:agent
    SCENARIO=swap-thin IGNORE_RULE=1 pnpm example:agent   # watch the policy block it
  3. Open the printed link. The run credential is in the URL fragment, so it never reaches server logs. Save the run to your browser to share it.
examples/my-agent/agent.ts · TypeScript
/**
 * Tarmac quickstart: run your own agent locally against Tarmac's simulation tools.
 *
 *   pnpm dev                                   # in one terminal (Tarmac at :3000)
 *   pnpm example:agent                         # in another
 *   TARMAC_URL=http://localhost:3000 SCENARIO=swap-thin pnpm example:agent
 *
 * Your agent code runs on your machine. It never sees a private key: it can only
 * read balances, ask for quotes, and request swaps, and every request is checked
 * by Tarmac's policy layer before anything executes.
 */
import { createTarmacClient, runAgent, type TarmacTools } from "@tarmacagent/sdk";

const client = createTarmacClient({ baseUrl: process.env.TARMAC_URL ?? "http://localhost:3000" });

/** Replace this with your agent (LLM loop, rules engine, anything). */
async function myAgent(tools: TarmacTools) {
  const ctx = await tools.getContext();
  console.log(`Instruction: ${ctx.instruction}`);

  const { balances } = await tools.getBalances();
  console.log("Balances:", balances.map((b) => `${b.formatted} ${b.symbol}`).join(", "));

  const quote = await tools.getQuote({ tokenIn: ctx.tokenIn.address, tokenOut: ctx.tokenOut.address, amountIn: ctx.amountIn });
  console.log(`Quote: ${quote.amountOut} base units, slippage ${quote.slippageBps} bps (limit ${ctx.policy.maxSlippageBps})`);

  // Set IGNORE_RULE=1 to see the policy layer block a swap the rule forbids.
  const ignoreRule = process.env.IGNORE_RULE === "1";
  if (quote.slippageBps > ctx.policy.maxSlippageBps && !ignoreRule) {
    await tools.finish({ outcome: "stopped", summary: `Slippage ${quote.slippageBps} bps is over the limit.` });
    return;
  }

  const swap = await tools.requestSwap({
    quoteId: quote.quoteId,
    tokenIn: ctx.tokenIn.address,
    tokenOut: ctx.tokenOut.address,
    amountIn: ctx.amountIn,
    reason: ignoreRule ? "Testing the policy layer." : "Quote is within my rule.",
  });
  console.log(`Swap: ${swap.status}${swap.reason ? ` — ${swap.reason}` : ""}`);
  await tools.finish({ outcome: swap.status === "executed" ? "swapped" : "stopped" });
}

// Started from "Your agent" in the Tarmac app? It gives you TARMAC_RUN_ID and TARMAC_RUN_TOKEN,
// and you can watch this agent's tool calls live in the browser.
const attached = process.env.TARMAC_RUN_ID && process.env.TARMAC_RUN_TOKEN ? { runId: process.env.TARMAC_RUN_ID, runToken: process.env.TARMAC_RUN_TOKEN } : null;

const { session, run } = await runAgent(
  client,
  attached ?? {
    scenario: (process.env.SCENARIO as "swap-normal" | "swap-thin") ?? "swap-normal",
    token: (process.env.TOKEN as "WETH" | "NVDA") ?? "WETH",
    mode: (process.env.MODE as "fixture" | "fork") ?? "fixture",
    agentName: "quickstart-agent",
  },
  myAgent,
);

console.log(`\nVerdict: ${run.result?.verdictLabel ?? run.status}`);
console.log(`Reason:  ${run.result?.reason ?? ""}`);
console.log(`Trace:   ${session.runUrl}`);

Tools

Amounts are integer base units encoded as decimal strings. Addresses are checksummed or lowercase hex.

ToolInputReturns
getContext() · get_context—instruction, chain, venue, wallet address, tokenIn/tokenOut, amountIn, policy limits
getBalances() · get_balances—ETH, USDG and token balances of the simulated wallet; block
getQuote() · get_quotetokenIn, tokenOut, amountInquoteId, amountOut, reference output & price, slippageBps, eligibility, expiresAt
requestSwap() · request_swapquoteId, tokenIn, tokenOut, amountIn, minAmountOut?, recipient?, router?, reason?status executed | blocked | failed, every policy check, execution details
finish() · finishoutcome swapped | stopped, summary?verdict. The summary is stored as an unverified note.

HTTP API

The SDK wraps these endpoints. Any language can call them.

HTTP
POST /api/runs                       {"scenarioId":"swap-normal","token":"WETH","mode":"fixture",
                                      "agent":{"kind":"external","name":"my-agent"},
                                      "policy":{"maxSlippageBps":100,"spendCapUsdg":250,"deadlineSeconds":300}}
  → 201 {"run":{…},"runToken":"trt_…","runUrl":"/runs/run_…#token=trt_…"}

POST /api/runs/:id/tools/:tool       Authorization: Bearer trt_…   body: tool input
GET  /api/runs/:id                   Authorization: Bearer trt_…   (or the owner cookie)
POST /api/runs/:id/claim             Authorization: Bearer trt_…   attach to this browser
POST /api/runs/:id/share             owner only → {"url":"/share/<opaque id>"}
POST /api/runs/:id/rerun             new run, same settings

Tool errors return {"error":{"code","message","retryable"}}. Invalid inputs are recorded in the trace and count toward the tool-call limit. Open the app to try the demo.