Documentation

Build on Outlaw

Everything the protocol does on-chain, the contracts it touches on Robinhood Chain, and a public REST API so terminals, bots, and aggregators can list Outlaw pairs without asking permission.

Introduction

Outlaw is a token launchpad on Robinhood Chain. One transaction deploys a token and opens a live market — no presale, no allowlist, no owner keys. Every launch is constrained the same way, by construction:

GuaranteeHow
Fixed supply1,000,000,000 tokens, minted once at deploy. No mint function exists afterwards.
No ownerGovernance is noOp — no admin, no upgrades, no taxes, no blacklist. Renounced from block one.
Fair distribution90% of supply is sold on a public bonding curve. The deployer buys on the same curve as everyone else.
Locked liquidityOn graduation the curve proceeds seed a Uniswap pool and the LP is locked. There is no withdraw path.

Everything is non-custodial: launches and trades are transactions signed by your wallet, settled on Robinhood Chain. The Outlaw app is a viewer and a transaction builder — it never holds funds.

How it works

Launches run on Doppler static auctions — audited launch infrastructure deployed on Robinhood Chain mainnet. A token's life has three phases, which map 1:1 to the columns on the /pairs board:

PhaseStatusWhat happens
1. CurvenewDeploy mints 1B tokens and puts 900M on a Uniswap v3 bonding curve priced in WETH, starting around a $10K market cap.
2. CompletingcompletingBuys walk the price up the curve. Progress is deterministic — the % filled is readable on-chain at any time.
3. MigratedmigratedAt ~$250K market cap the curve graduates: proceeds + remaining supply seed a Uniswap v2 pool and the LP is locked permanently. Trading continues on the open DEX.
Graduation is automatic and permissionless — no team action, no vote. Once the curve fills, migration is the only possible state transition.

Network

Robinhood Chain is an Arbitrum Orbit L2. Gas is paid in ETH and blocks arrive roughly every 100ms.

MainnetTestnet
Chain ID466346630
RPChttps://rpc.mainnet.chain.robinhood.comhttps://rpc.testnet.chain.robinhood.com
Explorerhttps://robinhoodchain.blockscout.comhttps://explorer.testnet.chain.robinhood.com
Gas tokenETHETH
Uniswap and Doppler are live on mainnet (4663) only. The testnet has neither yet, so launches and trading target mainnet.

Contracts

Core infrastructure on Robinhood Chain mainnet (4663):

ContractAddress
WETH90x0bd7d308f8e1639fab988df18a8011f41eacad73
UniswapV3Factory0x1f7d7550b1b028f7571e69a784071f0205fd2efa
SwapRouter020xcaf681a66d020601342297493863e78c959e5cb2
NonfungiblePositionManager0x73991a25c818bf1f1128deaab1492d45638de0d3
QuoterV20x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7

The Doppler launch contracts (airlock, factories, migrators) ship with the SDK — resolve the current set at runtime instead of hard-coding:

doppler addresses
import { getAddresses } from "@whetstone-research/doppler-sdk/evm";

const addrs = getAddresses(4663); // airlock, tokenFactory, v3Initializer, ...

Launching

The simplest path is the launch form: image, name, ticker, socials — one wallet signature. To launch programmatically, do exactly what the app does.

1 · Connect to Robinhood Chain

setup.ts — viem
import { createPublicClient, createWalletClient, custom, http, defineChain } from "viem";

export const robinhood = defineChain({
  id: 4663,
  name: "Robinhood Chain",
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
  blockExplorers: {
    default: { name: "Blockscout", url: "https://robinhoodchain.blockscout.com" },
  },
});

const publicClient = createPublicClient({ chain: robinhood, transport: http() });
const walletClient = createWalletClient({ chain: robinhood, transport: custom(window.ethereum) });

2 · Store token metadata

Post the off-chain metadata first — the response uri becomes the token's on-chain tokenURI, so wallets and explorers resolve the name, image, and links. Images are data URLs, downscaled client-side to ≤512px.

metadata
curl -X POST https://outlaw.markets/api/launch/metadata \
  -H "content-type: application/json" \
  -d '{
    "mint": "my-token-launch",
    "name": "My Token",
    "symbol": "MYT",
    "description": "Launched outside the system.",
    "image": "data:image/png;base64,..."
  }'

# → { "uri": "https://outlaw.markets/api/metadata/my-token-launch" }

3 · Deploy the static auction

One call deploys the token and its bonding curve. The parameters below are the Outlaw standard — same supply, same curve band, same migration for every launch.

launch.ts — doppler sdk
import { DopplerSDK, getAddresses } from "@whetstone-research/doppler-sdk/evm";
import { parseEther } from "viem";

const sdk = new DopplerSDK({ publicClient, walletClient, chainId: 4663 });

const params = sdk
  .buildStaticAuction()
  .tokenConfig({ name: "My Token", symbol: "MYT", tokenURI })
  .saleConfig({
    initialSupply: parseEther("1000000000"),   // fixed 1B — every Outlaw token
    numTokensToSell: parseEther("900000000"),  // 90% sold on the curve
    numeraire: getAddresses(4663).weth,        // priced in WETH
  })
  .withMarketCapRange({
    marketCap: { start: 10_000, end: 250_000 }, // USD — curve fills $10K → $250K
    numerairePrice: 3000,                       // ETH/USD reference
  })
  .withMigration({ type: "uniswapV2" })  // graduation: LP seeded + locked
  .withGovernance({ type: "noOp" })      // no owner, no upgrades, no taxes
  .withUserAddress(deployer)
  .build();

const { tokenAddress, poolAddress, transactionHash } =
  await sdk.factory.createStaticAuction(params);
tokenAddress is your ERC-20, poolAddress is the live curve. The token page and the API pick both up immediately — there is no registration step.

Trading

While a token is on the curve it trades in its Uniswap v3 pool against WETH; after graduation it trades in the migrated Uniswap v2 pool. Either way it's standard Uniswap — every router, aggregator, and bot that speaks Uniswap works unchanged, with standard pool fees.

Quoting

Quote through QuoterV2 (a simulate call, not a transaction), then swap through SwapRouter02 with your slippage bound:

quote.ts
import { parseAbi } from "viem";

const QUOTER_V2 = "0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7";

const quoterAbi = parseAbi([
  "function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) returns (uint256 amountOut, uint160, uint32, uint256)",
]);

const { result } = await publicClient.simulateContract({
  address: QUOTER_V2,
  abi: quoterAbi,
  functionName: "quoteExactInputSingle",
  args: [{ tokenIn: weth, tokenOut: token, amountIn, fee, sqrtPriceLimitX96: 0n }],
});

const minAmountOut = (result[0] * 99n) / 100n; // 1% slippage guard

For display prices without an RPC round-trip, use the REST API below.

REST API

A public read API for anyone integrating Outlaw pairs into an external terminal, screener, or bot. It serves the same data that powers the /pairs board and the token pages.

quickstart
# the live board — three columns in one call
curl "https://outlaw.markets/api/v1/tokens?status=new&limit=10"

# one token, by launch id or 0x contract address
curl "https://outlaw.markets/api/v1/tokens/kbm"

# chart data
curl "https://outlaw.markets/api/v1/tokens/kbm/candles?interval=5m"

Conventions

Base URLhttps://outlaw.markets/api/v1
AuthNone — all endpoints are public reads
CORSOpen (Access-Control-Allow-Origin: *) — call it straight from a browser terminal
FormatJSON. Prices in USD, timestamps in unix seconds, percentages as numbers (12.4 = +12.4%)
ErrorsNon-200 with { "error": "message" } — 400 for bad params, 404 for unknown tokens
CachingResponses set short cache-control max-age; respect it instead of hammering
Token idsEvery {id} accepts the launch id (slug) or the token's 0x contract address

List tokens

GET/api/v1/tokens

The board. Filter by lifecycle status to reproduce the three columns, or search across names and tickers.

ParamTypeDescription
statusstringFilter: new · completing · migrated
qstringSearch names and tickers
sortstringcreatedAt (default) · marketCap · volume24h · change24h · bondingProgress
orderstringdesc (default) · asc
limitnumber1–100, default 50
offsetnumberPagination offset
200 — GET /api/v1/tokens?status=new&limit=10
{
  "tokens": [
    {
      "id": "kbm",
      "name": "Keyboard Monkey",
      "symbol": "KBM",
      "status": "new",
      "priceUsd": 0.0000084,
      "marketCapUsd": 8400,
      "liquidityUsd": 5100,
      "volume24hUsd": 6200,
      "change24hPct": 12.4,
      "txns24h": 31,
      "holders": 14,
      "bondingProgressPct": 3,
      "createdAt": 1783987158
    }
  ],
  "pagination": { "total": 8, "limit": 10, "offset": 0 }
}

Token detail

GET/api/v1/tokens/{id}

Everything the board row has, plus addresses, supply, safety facts, and links to the sub-resources.

200 — GET /api/v1/tokens/kbm
{
  "token": {
    "id": "kbm",
    "name": "Keyboard Monkey",
    "symbol": "KBM",
    "status": "new",
    "priceUsd": 0.0000084,
    "marketCapUsd": 8400,
    "liquidityUsd": 5100,
    "volume24hUsd": 6200,
    "change24hPct": 12.4,
    "txns24h": 31,
    "holders": 14,
    "bondingProgressPct": 3,
    "createdAt": 1783987158,
    "description": "Fixed 1B supply, mint authority revoked at deploy...",
    "tokenAddress": "0x7c61...",
    "deployerAddress": "0x94b0...",
    "totalSupply": 1000000000,
    "chainId": 4663,
    "graduationMarketCapUsd": 250000,
    "ownershipRenounced": true,
    "liquidityLocked": false,
    "links": {
      "self": "/api/v1/tokens/kbm",
      "candles": "/api/v1/tokens/kbm/candles",
      "trades": "/api/v1/tokens/kbm/trades",
      "holders": "/api/v1/tokens/kbm/holders",
      "page": "/token/kbm"
    }
  }
}

Candles

GET/api/v1/tokens/{id}/candles

OHLCV bars for charting — drop them straight into a TradingView/lightweight-charts series. Base resolution is 1 minute; higher intervals are aggregated server-side.

ParamTypeDescription
intervalstring1m (default) · 5m · 15m · 1h
limitnumber1–1000, default 500 (most recent bars)
200 — GET /api/v1/tokens/kbm/candles?interval=5m
{
  "id": "kbm",
  "interval": "5m",
  "candles": [
    {
      "time": 1783985700,
      "open": 0.0000081,
      "high": 0.0000086,
      "low": 0.0000079,
      "close": 0.0000084,
      "volume": 41.2
    }
  ]
}

Trades

GET/api/v1/tokens/{id}/trades

Most recent fills, newest first. limit 1–100, default 50.

200 — GET /api/v1/tokens/kbm/trades
{
  "id": "kbm",
  "trades": [
    {
      "side": "buy",
      "amountToken": 402519,
      "amountUsd": 3381.16,
      "priceUsd": 0.0000084,
      "wallet": "0x3f9a...",
      "timestamp": 1783987121
    }
  ]
}

Holders

GET/api/v1/tokens/{id}/holders

Top holders by supply share. Special positions carry a tag: lp (the locked pool) and deployer.

200 — GET /api/v1/tokens/kbm/holders
{
  "id": "kbm",
  "totalHolders": 14,
  "holders": [
    { "wallet": "0xa1b2...", "pctSupply": 6.4, "tag": "lp" },
    { "wallet": "0x94b0...", "pctSupply": 2.1, "tag": "deployer" },
    { "wallet": "0x88c3...", "pctSupply": 1.9 }
  ]
}

Protocol stats

GET/api/v1/stats

Protocol-wide totals for dashboards and tickers.

200 — GET /api/v1/stats
{
  "chainId": 4663,
  "launchesTotal": 18920,
  "migratedTotal": 1284,
  "volume24hUsd": 23400000,
  "liquidityRoutedUsd": 9640000,
  "board": { "new": 8, "completing": 6, "migrated": 14 }
}

Gotchas

Testnet ≠ mainnetChain 46630 has no Uniswap or Doppler deployment. Test against mainnet with small size, or fork it locally.
block.number is L1-pacedRobinhood Chain is an Arbitrum Orbit rollup: the block.number opcode tracks the parent chain (~10s+ cadence) while RPC blocks land every ~100ms. Never use it for fine-grained timing — use block.timestamp.
Metadata before launchPost /api/launch/metadata first and pass the returned uri as tokenURI — a launch with a dead URI renders blank in wallets forever (the token is immutable).
Address casingThe API returns lowercase 0x addresses and accepts either casing; contract calls via viem want checksummed or lowercase, not mixed.
Immutable means immutableNo function exists to change a token's name, image, taxes, or supply after deploy. Triple-check the ticker before signing.
Quote before every swapCurve prices move fast near graduation. Re-quote via QuoterV2 right before sending, and always set amountOutMinimum.