Developer docs

API reference

Request data and swap routes on Base. Pay in USDC with x402.

https://api.beamswap.ioOpenAPI JSON

Quickstart

Send a request. A 402 response tells you what to pay.

Swap route request
curl -i -X POST "https://api.beamswap.io/v1/execute/route" -H "content-type: application/json" -d '{"sell":"ETH","buy":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","amount":"1000000000000000000","from":"0xe598c65f960a8c39b539f31cabf2c28f1567fd54","recipient":"0xe598c65f960a8c39b539f31cabf2c28f1567fd54","slippageBps":50}'
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)

Payments

Sign the requested USDC payment and retry. Successful paid responses include a receipt.

Every paid endpoint answers 402 Payment Required until the request carries a valid payment. The 402 lists what to pay in its PAYMENT-REQUIRED header (base64 JSON): the USDC amount, the network (Base, eip155:8453), the treasury address and a timeout.

Your client signs an EIP-3009 USDC transfer for that amount and retries the same request with a PAYMENT-SIGNATURE header. A facilitator verifies the signature, the request is served, and the transfer is settled on-chain after the handler succeeds. The facilitator pays the gas.

You are never charged for a failed request: validation errors (400) are answered before payment is requested, and upstream failures (502) cancel the payment. The PAYMENT-RESPONSE header on a successful response is your receipt and contains the settlement transaction hash.

USDC is the only payment asset. Prices shown in this reference are list prices. When a session bearer resolves to a GLINT tier, the 402 accepts[0].amount contains the discounted micro-USDC amount. The discount is applied once, including to dynamic watch and distribution prices.

Any x402 client
import { wrapFetchWithPayment, x402Client } from "@x402/fetch"
import { ExactEvmScheme } from "@x402/evm"
import { privateKeyToAccount } from "viem/accounts"

const client = new x402Client().register("eip155:*", new ExactEvmScheme(privateKeyToAccount(KEY)))
const paidFetch = wrapFetchWithPayment(fetch, client)
const res = await paidFetch("https://api.beamswap.io/v1/quote?sell=ETH&buy=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=1000000000000000000")

Sessions and quota

Sign in with your wallet to use its daily allowance. A session lasts 24 hours.

A session is a 24-hour bearer token bound to one wallet address. Allowlisted wallets, active GLINT stakers and eligible snapshot claimants use it to skip payment while quota remains. When quota runs out the same request falls back to the 402 flow with that tier’s discount already reflected in accepts[0].amount.

Sign the EIP-712 message below with the wallet, then POST it to /v1/session. Send the returned token as Authorization: Bearer <token>. Responses that used quota carry an x-quota-remaining header.

issuedAt is the current unix time in seconds and must be within 5 minutes of server time. A stale issuedAt or a signature that does not match the address is answered 401.

Manage your stake at /stake. The configured contract is 0x0A18f9F3E047E606C17CfcE7C8e17bFF87a4D5a3. Only active stake counts toward the tier; an unstaking request moves that amount into a pending bucket for seven days. A second request adds to the pending amount and resets the cooldown for the whole bucket.

Snapshot claimant credit starts on the launch date and ends exactly 90 days later. It never activates before launch. The highest tier from the allowlist, claimant credit and active stake applies.

EIP-712 message
{
  "domain": { "name": "Beamswap Agent API", "version": "1", "chainId": 8453 },
  "types": { "Session": [{ "name": "address", "type": "address" }, { "name": "issuedAt", "type": "uint256" }] },
  "primaryType": "Session",
  "message": { "address": "0xYourWallet", "issuedAt": 1789000000 }
}
POST /v1/session
curl -X POST https://api.beamswap.io/v1/session \
  -H "content-type: application/json" \
  -d '{"address":"0xYourWallet","issuedAt":1789000000,"signature":"0x…"}'
# → { "token": "…", "expiresAt": 1789086400 }

Endpoints

Choose an endpoint below for parameters and examples.

GET

/v1/token/{address}

$0.005

Token facts for a Base ERC-20: metadata, supply, deployer, price, round-trip quote

addresspath · required

ERC-20 contract address

0x55B423D0189F2315073DFb49845ea9eFFD9815A4
Request
curl -i "https://api.beamswap.io/v1/token/0x55B423D0189F2315073DFb49845ea9eFFD9815A4"
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)
Response 200
{
  "address": "0x55b423d0189f2315073dfb49845ea9effd9815a4",
  "name": "Beamswap Token",
  "symbol": "GLINT",
  "decimals": 18,
  "totalSupply": "1941031836082027011050588144",
  "deployer": {
    "address": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
    "txHash": "0x9e1c0a3f2b7d4e5a6c8f0b1d2e3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a"
  },
  "usdPrice": 0.0123,
  "liquidity": {
    "buyQuoteOk": true,
    "sellQuoteOk": true,
    "roundTripLossBps": 180
  },
  "asOf": "2026-09-14T00:00:00.000Z"
}
GET

/v1/portfolio/{address}

$0.02

ETH + ERC-20 balances of a Base address with USD values

addresspath · required

Wallet address

0xe598c65f960a8c39b539f31cabf2c28f1567fd54
Request
curl -i "https://api.beamswap.io/v1/portfolio/0xe598c65f960a8c39b539f31cabf2c28f1567fd54"
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)
Response 200
{
  "address": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
  "chain": "base",
  "asOf": "2026-09-14T00:00:00.000Z",
  "totalUsd": "1002.00",
  "assets": [
    {
      "contract": "native",
      "symbol": "ETH",
      "decimals": 18,
      "balance": "500000000000000000",
      "usdPrice": 2000,
      "usdValue": "1000.00"
    }
  ]
}
GET

/v1/quote

$0.005

Best swap output across Base aggregators (quote only, no calldata)

sellquery · required

Token address or ETH

ETH
buyquery · required

Token address or ETH

0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
amountquery · required

Sell amount in the sell token’s smallest unit

1000000000000000000
slippageBpsquery · optional

1..5000, default 50

50
Request
curl -i "https://api.beamswap.io/v1/quote?sell=ETH&buy=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=1000000000000000000&slippageBps=50"
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)
Response 200
{
  "sell": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
  "buy": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "amountIn": "1000000000000000000",
  "slippageBps": 50,
  "best": {
    "source": "kyberswap",
    "amountOut": "2010000000",
    "minOut": "1999950000"
  },
  "sources": [
    {
      "source": "kyberswap",
      "amountOut": "2010000000",
      "error": null
    },
    {
      "source": "0x",
      "amountOut": "2008400000",
      "error": null
    }
  ]
}
POST

/v1/execute/route

$0.01

Best swap route as ready-to-sign calldata, Beamswap fee included

sellbody · required

Token address or ETH

ETH
buybody · required

Token address or ETH

0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
amountbody · required

Sell amount in the sell token’s smallest unit, as a decimal string

1000000000000000000
frombody · required

Address that will sign and send the transaction

0xe598c65f960a8c39b539f31cabf2c28f1567fd54
recipientbody · optional

Receiver of the output token; defaults to from

0xe598c65f960a8c39b539f31cabf2c28f1567fd54
slippageBpsbody · optional

1..2000, default 50

50
Body
{
  "sell": "ETH",
  "buy": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "amount": "1000000000000000000",
  "from": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
  "recipient": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
  "slippageBps": 50
}
Request
curl -i -X POST "https://api.beamswap.io/v1/execute/route" -H "content-type: application/json" -d '{"sell":"ETH","buy":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","amount":"1000000000000000000","from":"0xe598c65f960a8c39b539f31cabf2c28f1567fd54","recipient":"0xe598c65f960a8c39b539f31cabf2c28f1567fd54","slippageBps":50}'
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)
Response 200
{
  "sell": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
  "buy": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "amountIn": "1000000000000000000",
  "from": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
  "recipient": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
  "slippageBps": 50,
  "feeBps": 10,
  "feeRecipient": "0x1111111111111111111111111111111111111111",
  "deadline": 1789000060,
  "best": {
    "source": "kyberswap",
    "to": "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5",
    "data": "0x…",
    "value": "1000000000000000000",
    "gas": "210000",
    "allowanceTarget": null,
    "expectedOut": "2008000000",
    "minOut": "1997960000",
    "feeAmount": null,
    "simulated": true,
    "simulation": {
      "ok": true,
      "reason": null
    }
  },
  "sources": [
    {
      "source": "kyberswap",
      "expectedOut": "2008000000",
      "error": null
    },
    {
      "source": "0x",
      "expectedOut": "2006400000",
      "error": null
    }
  ],
  "notes": [
    "Base has no public mempool, so sandwiching is limited to the sequencer. The route sets minOut and a 60 s deadline; that is the protection offered."
  ]
}
POST

/v1/watch

$0.01

Watch Base balances or USD prices; signed webhook when a condition trips

itemsbody · required

Conditions to watch, 1..50. Each is {type: balance_below|balance_above, address, token (address or "native"), threshold (wei string)} or {type: price_cross, token, usd, direction: above|below}.

[{"type":"balance_below","address":"0x…","token":"native","threshold":"100000"}]
daysbody · required

How long to watch, 1..90 days. The price is $0.01 per item per day.

3
webhookUrlbody · required

https URL on a public host, no credentials, max 2048 chars. Each trip POSTs the payload signed as x-beamswap-signature: sha256=HMAC-SHA256(secret, rawBody).

https://hooks.example.com/beamswap
Body
{
  "items": [
    {
      "type": "balance_below",
      "address": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
      "token": "native",
      "threshold": "100000000000000000"
    },
    {
      "type": "price_cross",
      "token": "0x55B423D0189F2315073DFb49845ea9eFFD9815A4",
      "usd": 0.02,
      "direction": "above"
    }
  ],
  "days": 3,
  "webhookUrl": "https://hooks.example.com/beamswap"
}
Request
curl -i -X POST "https://api.beamswap.io/v1/watch" -H "content-type: application/json" -d '{"items":[{"type":"balance_below","address":"0xe598c65f960a8c39b539f31cabf2c28f1567fd54","token":"native","threshold":"100000000000000000"},{"type":"price_cross","token":"0x55B423D0189F2315073DFb49845ea9eFFD9815A4","usd":0.02,"direction":"above"}],"days":3,"webhookUrl":"https://hooks.example.com/beamswap"}'
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)
Response 200
{
  "ids": [
    "6f1f3a1e-9e2b-4a5c-8d7e-0b1c2d3e4f5a",
    "7a2e4b2f-0f3c-4b6d-9e8f-1c2d3e4f5a6b"
  ],
  "secret": "f3c1…64 hex chars…9ab2",
  "expiresAt": "2026-09-18T00:00:00.000Z",
  "price": "$0.06",
  "webhookUrl": "https://hooks.example.com/beamswap"
}
POST

/v1/distribution

$50

Deploy a hosted ERC-20 merkle distribution on Base

tokenbody · required

ERC-20 token contract address on Base

0x55B423D0189F2315073DFb49845ea9eFFD9815A4
entriesbody · required

1..100000 unique recipients with amounts in the token smallest unit

[{"address":"0xe598…","amount":"1000000000000000000"}]
deadlinebody · optional

Future Unix timestamp, at most 365 days away; defaults to 90 days

1790000000
namebody · optional

Distribution name, at most 80 characters

Community rewards
Body
{
  "token": "0x55B423D0189F2315073DFb49845ea9eFFD9815A4",
  "entries": [
    {
      "address": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
      "amount": "1000000000000000000"
    }
  ],
  "deadline": 1790000000,
  "name": "Community rewards"
}
Request
curl -i -X POST "https://api.beamswap.io/v1/distribution" -H "content-type: application/json" -d '{"token":"0x55B423D0189F2315073DFb49845ea9eFFD9815A4","entries":[{"address":"0xe598c65f960a8c39b539f31cabf2c28f1567fd54","amount":"1000000000000000000"}],"deadline":1790000000,"name":"Community rewards"}'
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)
Response 200
{
  "id": "6f1f3a1e-9e2b-4a5c-8d7e-0b1c2d3e4f5a",
  "contract": "0x0000000000000000000000000000000000000001",
  "root": "0x…",
  "total": "1000000000000000000",
  "rows": 1,
  "feeBps": 10,
  "fundTx": {
    "grossAmount": "1001001001001001002",
    "approve": {
      "to": "0x…token",
      "data": "0x…",
      "value": "0"
    },
    "fund": {
      "to": "0x…factory",
      "data": "0x…",
      "value": "0"
    },
    "unpause": {
      "to": "0x…distributor",
      "data": "0x…",
      "value": "0"
    }
  },
  "claimUrl": "https://app.beamswap.io/d/6f1f3a1e-9e2b-4a5c-8d7e-0b1c2d3e4f5a"
}
GET

/v1/distribution/{id}/proof/{address}

$0.001

Proof and cumulative allocation for one distribution recipient

idpath · required

Distribution id returned by distribution creation

6f1f3a1e-9e2b-4a5c-8d7e-0b1c2d3e4f5a
addresspath · required

Recipient wallet address

0xe598c65f960a8c39b539f31cabf2c28f1567fd54
Request
curl -i "https://api.beamswap.io/v1/distribution/6f1f3a1e-9e2b-4a5c-8d7e-0b1c2d3e4f5a/proof/0xe598c65f960a8c39b539f31cabf2c28f1567fd54"
# → 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE (any x402 client does this for you)
Response 200
{
  "address": "0xe598c65f960a8c39b539f31cabf2c28f1567fd54",
  "cumulativeAmount": "1000000000000000000",
  "proof": [
    "0x…"
  ],
  "root": "0x…",
  "contract": "0x0000000000000000000000000000000000000001"
}

Execution routes

Review the route before signing. Native ETH sells include a dry run. ERC-20 sells require any needed approval and are not simulated yet.

Base has no public mempool, so sandwiching is limited to the sequencer. The route sets minOut and a 60 s deadline; that is the protection offered.

For ERC-20 sells, approve allowanceTarget for at least amountIn before sending. ETH sells need no approval and are simulated from your address before the route is returned.

from must already hold amount (ETH for native sells) when you call: the dry run is made from that address, and an unfunded from is answered 502 and not charged. A recipient different from from excludes 0x from the fan-out; KyberSwap still routes.

gas is the dry run’s estimate with no margin; add your own before sending. allowanceTarget is null for ETH sells and when the aggregator reports the allowance already in place.

The fee is taken from the output token inside the route; expectedOut and minOut are after the fee. Tiers 0 and 1 pay 10 bps, tier 2 pays 5 bps and tier 3 pays 3 bps.

Monitoring

Watch a balance or price threshold. Verify the signature on every webhook.

Three conditions can be watched. balance_below and balance_above take address, token (a token address, or "native" for ETH) and threshold, a wei value as a decimal string. price_cross takes token, usd and direction (above or below), priced by the same feed /v1/portfolio values balances with. lp_position_drift and token_unlock are deferred: they need per-protocol position readers and vesting ABIs that do not exist yet, and a paid watch that cannot observe its condition would be selling nothing.

POST /v1/watch takes 1 to 50 items, days between 1 and 90, and a webhookUrl. Every item becomes its own watch with its own id and its own state; they share one secret and one webhook URL. The response is the only place that secret is ever shown, so store it when you get it.

Price: $0.01 per item per day, charged once at creation for the whole window. Fifty items for ninety days is $45, which is the amount the 402 asks for and the amount settled in one transfer. DELETE /v1/watch/:id stops a watch immediately and refunds nothing: the days were bought up front. GET and DELETE are free and take no auth, because the id is the capability that reads or cancels the watch. The GET returns the condition, status, last observed value, expiry and the recent deliveries; it never returns the secret, the webhook URL or the payer.

Watches are edge-triggered. One webhook is sent when the condition becomes true, and nothing more until the condition has been observed false again. The first observation counts: a balance already below its threshold when the watch is created trips on the first poll. Polling is every 30 seconds by default (AGENT_WATCH_POLL_SECONDS), so a condition that becomes true and false again between two polls is never seen.

A trip is one POST to webhookUrl with content-type application/json, x-beamswap-watch carrying the watch id, and x-beamswap-signature carrying sha256= followed by the hex HMAC-SHA256 of the exact bytes posted, keyed with the watch secret. Verify over the raw body, never over a re-serialised object: a key order or a space that differs changes the signature.

A 2xx marks the delivery done. Anything else is retried after 1, 5, 25, 125 and 625 seconds, five retries in all, and then the delivery is marked failed. Each attempt has a 10 second timeout and at most 64 KB of your answer is read. Redirects are never followed: a 3xx fails the delivery outright, because the public-address check ran against the URL you gave and following a hop would step around it.

Delivery is at least once, not exactly once: a trip is queued before the state that disarms it, so a failure in between can repeat one webhook. Dedupe on id and at together. Deleting a watch drops the deliveries still queued for it, but one already in flight can still arrive, so a webhook landing just after a delete is not an error.

webhookUrl must be an absolute https URL with no credentials, at most 2048 characters, on a host that resolves only to public addresses. Private ranges, loopback, link-local (the cloud metadata address included) and CGNAT are refused when the watch is created. The host is resolved by DNS once, at creation, so a name that exists only in the receiving machine’s hosts file does not resolve and is refused.

Webhook payload
POST /your-hook HTTP/1.1
content-type: application/json
x-beamswap-watch: 0f2c…
x-beamswap-signature: sha256=9a1b…

{
  "id": "0f2c…",
  "type": "balance_below",
  "value": "913000000000000000",
  "threshold": "1000000000000000000",
  "at": "2026-09-15T09:14:03.000Z",
  "params": { "type": "balance_below", "address": "0x…", "token": "native", "threshold": "1000000000000000000" }
}
Verify the signature (Node)
import { createHmac, timingSafeEqual } from "node:crypto"

// rawBody is the exact bytes that were posted, read before any JSON.parse.
const expected = createHmac("sha256", secret).update(rawBody).digest("hex")
const sent = (req.headers["x-beamswap-signature"] ?? "").slice("sha256=".length)
// Check the shape first: Buffer.from drops anything that is not hex, and timingSafeEqual
// throws on buffers of different lengths.
const ok =
  /^[0-9a-f]{64}$/.test(sent) &&
  timingSafeEqual(Buffer.from(sent, "hex"), Buffer.from(expected, "hex"))

Distributions

Create a distribution at /distribute, fund it and share the claim link. Deployment must be configured before this feature is available; the create endpoint returns 503 until the factory and deployer are configured.

POST /v1/distribution takes an ERC-20 address and 1 to 100,000 unique recipient allocations in the token’s smallest unit. The $50 x402 payment covers building the cumulative Merkle tree and mining a dedicated distributor clone on Base. The paying wallet becomes the clone owner. The response is returned only after the deployment is mined and contains the hosted claim URL plus three owner transactions: approve the factory, fund through the factory, and unpause the clone.

Funding pays a 10 bps factory fee by default. The returned fund amount is grossed up so the distributor receives the entire promised allocation after the fee. The summed allocation and the gross amount required at the maximum allowed 100 bps fee must both fit uint256; larger requests are rejected before payment. Confirm the clone balance is at least total and test a stored proof before sending unpause. Fee-on-transfer tokens are unsupported because their transfer tax can leave the clone underfunded.

Claims are cumulative and self-claim only: the recipient wallet calls claim for its own address, and a later valid root can increase or replace its cumulative amount without resetting what it already claimed. The owner can call setMerkleRoot on-chain, but that call cannot update proofs stored by the API. Do not change the root alone. The stored leaves and proofs must be synchronized before the claim page can serve the new root; this release has no distribution root-update endpoint, so coordinate that correction with the operator.

The clone starts paused. It accepts claims only after the owner unpauses it and before the deadline. After the deadline, the owner can sweep unclaimed tokens. GET /v1/distribution/:id is free, while each agent proof costs $0.001. The hosted page uses a server-only proof route. The owner can download claims.csv with a session bearer token for the same wallet address.

The hosted claim page exposes guarded owner controls for funding, pause, unpause, early end and post-deadline sweep. The owner can also change the root directly on-chain, so a hosted page does not guarantee that every listed allocation will remain claimable.

Errors

Check the status code and response before retrying.

400: invalid input. Answered before any payment is requested.

401: the session signature is invalid or issuedAt is outside the 5 minute window. Nothing was charged.

402: payment required, or the payment could not be verified or settled. Nothing was charged.

500: an unexpected error on our side. Nothing was charged; the payment was cancelled.

502: an upstream provider (RPC, aggregator, price feed) failed or timed out. Nothing was charged.

503: the endpoint is not configured on this deployment.

Token amounts are always decimal strings in the smallest unit of the token, never JSON numbers, so nothing is lost above 2^53. USD prices are JSON numbers; USD values are two-decimal strings.

Fields that come from a third-party source (deployer, usdPrice, usdValue, roundTripLossBps, a source’s amountOut) are null when that source had no answer; the call still succeeds and is charged.

MCP

Hosted MCP is the recommended connection. Add https://api.beamswap.io/mcp to a compatible client. Every paid tool request opens a browser page where you review the tool, price and wallet payment before approving.

The initial hosted set covers token info, portfolios, swap quotes and routes, distribution proofs and status, watch status and deletion, and approval status. Watch deletion costs $0 but still asks for explicit approval. Create a watch through local MCP or the API. Create a distribution on the Beamswap website, through local MCP, or through the API.

For local stdio, run the pinned package with npx -y @beamswapio/mcp@0.1.0 on Node 22.13 or newer. On Windows, use npx.cmd when the client requires an executable name. The standalone BeamSwap/beamswap-mcp repository remains available as a source-build fallback with pnpm 10.

Local paid calls use BEAMSWAP_WALLET_KEY from your client configuration or environment, never chat. Each tool can automatically pay within its listed price ceiling; distribution creation can cost up to $50 per request. An uncertain signed payment creates a persistent wallet lock and blocks more paid calls until the owner checks the outcome.

See /start for client-specific hosted, local, private-tunnel and self-hosted setup. Direct REST calls are also available, but they are not MCP.

Hosted MCP URL
https://api.beamswap.io/mcp