# PulseSwap for bots & AI agents

## Who this is for

A program with no browser: a trading bot, a backend service, or a coding agent wiring a swap into someone else's app. The quote API takes JSON, needs no API key and no account, and hands back the transaction data your own signer sends. Nothing here custodies funds or keys.

> **This API is PulseChain only.** Chain 369, and any other `chainId` is rejected with a 400. The 17-chain reach you see on the site is the web app's own adapters, which are not part of this API or of the SDK — do not plan a multi-chain agent around either.

That narrowness is the reason the page exists. PulseChain is not covered by LI.FI, 1inch or 0x, so an agent that reaches chain 369 through one of those simply fails; this is the way to route on it programmatically.

## One call, best route

`POST https://quotes.pulseswap.io/api/v2/quotes/advanced` with `"platform": "mixed"`. The service prices the route across the PulseChain DEXes it indexes, splits it where splitting pays, and returns the one that pays out the most — you do not pick a DEX, compare answers, or run the search yourself.

```bash
curl -X POST https://quotes.pulseswap.io/api/v2/quotes/advanced \
  -H "Content-Type: application/json" \
  -H "User-Agent: my-bot/1.0 (+https://example.com)" \
  -d '{
    "chainId": 369,
    "platform": "mixed",
    "fromToken": "0xA1077a294dDE1B09bB078844df40758a5D0f9a27",
    "toToken": "0x95B303987A60C71504D99Aa1b13B4DA07b0790ab",
    "amountIn": "1000000000000000000",
    "slippage": 0.5,
    "userAddress": "0x742d35Cc6634C0532925a3b8D221691B5c1b6b29",
    "extra": { "gasPrice": "12345667890" }
  }'
```

```json
{
  "success": true,
  "data": {
    "success": true,
    "quoteId": "b7c4ee4a-1dd6-4a4f-9003-fcaae0593fb2",
    "amountIn": "1000000000000000000",
    "amountOut": "1401565066428738108",
    "amountOutUSD": "0",
    "gasEstimate": 391968,
    "tx": {
      "from": "0x742D35Cc6634C0532925A3B8D221691b5C1b6b29",
      "to": "0xC994375187988C751C8fCb96A68A0f242947f0E6",
      "data": "0x2d09aed500000000000000…",
      "value": "0"
    }
  },
  "message": "OK",
  "timestamp": "2026-08-19T06:47:54.152610116Z"
}
```

> **⚠️ Always send `extra`.** Leave the object out and two things go wrong at once: `userAddress` stops reaching `tx.from`, which leaves calldata you cannot send, and the search runs with no gas price. One field — `extra.gasPrice` in wei, as a string — is enough. Add `extra.tokenInPrice` and `extra.tokenOutPrice` in USD when you have them: they are what fills `amountOutUSD` in and let the service rank routes by net value.

- **Native PLS** is `0x0000000000000000000000000000000000000000` in `fromToken` or `toToken`. Sending it in puts the amount in `tx.value` instead of needing an approval.
- **`amountIn` is a wei string**, never a number — a large value does not survive a JavaScript number.
- **`slippage` is a percent** between 0.0 and 100.0: `0.5` means 0.5%.
- **`tx.to` is always the PulseSwap router**, `0xC994375187988C751C8fCb96A68A0f242947f0E6`, whichever DEXes the route ends up crossing — so one approval covers every route.

## The bot flow

Three steps, and the last one is yours alone:

1. **Quote without `userAddress`** — to price a pair, watch a spread, or decide whether to act at all. You get `amountOut` and `gasEstimate`; the `tx` that comes back carries the zero address and is not meant to be sent.
2. **Quote again with `userAddress`** once you have decided. Same call, and now `tx` is built for your account.
3. **Approve if needed, then sign and send.** An ERC-20 going in needs one approval with the router as spender; after that you sign `tx` with your own key and broadcast it.

```ts
import { createPublicClient, createWalletClient, erc20Abi, http, maxUint256 } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { pulsechain } from 'viem/chains';

const WPLS = '0xA1077a294dDE1B09bB078844df40758a5D0f9a27' as const;
const PLSX = '0x95B303987A60C71504D99Aa1b13B4DA07b0790ab' as const;

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const pub = createPublicClient({ chain: pulsechain, transport: http() });
const wallet = createWalletClient({ account, chain: pulsechain, transport: http() });

// 1. Ask for the best route, with calldata built for this account.
const res = await fetch('https://quotes.pulseswap.io/api/v2/quotes/advanced', {
  method: 'POST',
  headers: { 'content-type': 'application/json', 'user-agent': 'my-bot/1.0' },
  body: JSON.stringify({
    chainId: 369,
    platform: 'mixed',
    fromToken: WPLS,
    toToken: PLSX,
    amountIn: '1000000000000000000',
    slippage: 0.5,
    userAddress: account.address,
    extra: { gasPrice: (await pub.getGasPrice()).toString() },
  }),
});
const { data: quote } = await res.json();
// No route (a platform can't price this pair): HTTP 200, data.success=false, tx=null. Check it.
if (!quote?.success || !quote.tx) throw new Error('no route for this pair');

// 2. ERC-20 going in: approve the router once and WAIT for it. Native PLS needs no approval.
const approveHash = await wallet.writeContract({
  address: WPLS, abi: erc20Abi, functionName: 'approve', args: [quote.tx.to, maxUint256],
});
await pub.waitForTransactionReceipt({ hash: approveHash });

// 3. Sign and send it yourself. The keys never leave your process.
const hash = await wallet.sendTransaction({
  to: quote.tx.to, data: quote.tx.data, value: BigInt(quote.tx.value),
});
```

> **Quotes are short-lived.** Fetch, then send promptly — a route priced a block ago is a different route. Treat `gasEstimate` as a planning figure rather than a limit to send as-is, and keep `quoteId` in your logs: it is what lets anyone trace a specific quote later.

## Common token addresses

The ones a bot asks for first. PLS is the zero address and travels in `tx.value`; every other row is an ERC-20 that needs a one-time `approve` to the router. Mind the decimals — HEX and WBTC are 8, USDC and USDT are 6. The full list (8,800+ tokens, with logos): https://assets.pulsecoinlist.com/json/tokensList-369.json

| Symbol | Address | Decimals | Note |
|---|---|---|---|
| PLS | `0x0000000000000000000000000000000000000000` | 18 | native — sent in tx.value |
| WPLS | `0xA1077a294dDE1B09bB078844df40758a5D0f9a27` | 18 | Wrapped PLS |
| PLSX | `0x95B303987A60C71504D99Aa1b13B4DA07b0790ab` | 18 | PulseX |
| HEX | `0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39` | 8 | HEX |
| INC | `0x2fa878Ab3F87CC1C9737Fc071108F904c0B0C95d` | 18 | Incentive |
| DAI | `0xefD766cCb38EaF1dfd701853BFCe31359239F305` | 18 | DAI from Ethereum |
| USDC | `0x15D38573d2feeb82e7ad5187aB8c1D52810B1f07` | 6 | USDC from Ethereum |
| USDT | `0x0Cb6F5a34ad42ec934882A05265A7d5F59b51A2f` | 6 | USDT from Ethereum |
| WETH | `0x02DcdD04e3F455D838cd1249292C58f3B79e3C3C` | 18 | WETH from Ethereum |
| WBTC | `0xb17D901469B9208B17d916112988A3FeD19b5cA1` | 8 | WBTC from Ethereum |

## Single-platform quotes

Naming one DEX instead of `mixed` asks that DEX alone. It is the call for a bot that already knows where it wants to trade — comparing a single pool against the routed answer, or executing on a DEX it has its own reasons to prefer. The identifiers, from the SDK's own list:

| `platform`      | Type   | Routes through            |
| --------------- | ------ | ------------------------- |
| `pulsex_v1`     | uni-v2 | PulseX V1                 |
| `pulsex_v2`     | uni-v2 | PulseX V2                 |
| `pulsex_stable` | stable | PulseX Stable             |
| `9inch_v2`      | uni-v2 | 9inch v2                  |
| `9inch_v3`      | uni-v3 | 9inch v3                  |
| `9mm_v2`        | uni-v2 | 9mm v2                    |
| `9mm_v3`        | uni-v3 | 9mm v3                    |
| `phux_v2`       | bal-v2 | Phux.io                   |
| `tide_v3`       | bal-v3 | 0xTide                    |
| `mixed`         | Mixed  | Every row above, priced together |

`POST /quotes` is the same request against the standard algorithm rather than the deeper search. It answers faster; on a `mixed` request it is also the weaker of the two, so reach for it only when you have named a single DEX. Every schema, validation rule and error shape is written out on the [API reference](https://pulseswap.io/docs/api).

> 🤝 **Piteas is a separate service, not a `platform` value.** Its own API whitelists by `Referer`, so calling `api.piteas.io` from a script does not answer — which is why the SDK snippet below points `piteasUrl` at our proxy, `api.pulseswap.io/prod/piteasQuote`, and that one does. You only need it if you want Piteas as a *second* opinion: a `mixed` request already competes every routed DEX in the one call.

## The SDK

If you are writing TypeScript, `pulseswap-sdk` is the same service behind a typed client, and it is what this site's own swap uses. It is PulseChain only, like the API under it.

```ts
import { PulseSwapSDK, Platform, QuoteMode } from 'pulseswap-sdk';

const sdk = new PulseSwapSDK({
  quoteUrl: 'https://quotes.pulseswap.io/api/v2',
  piteasUrl: 'https://api.pulseswap.io/prod/piteasQuote',
});

// Platform.PULSESWAP goes over the wire as "mixed", and QuoteMode.OPTIMAL is
// what selects /quotes/advanced. Any other mode returns null without a request.
const quote = await sdk.getQuote(
  { chainId: 369, fromToken, toToken, userAddress, amountIn, slippage: 0.5, mode: QuoteMode.OPTIMAL },
  Platform.PULSESWAP,
);
```

`getQuote` resolves to the quote or to `null` — it does not throw — so a null check is not optional. Every type, enum value and behaviour is written out on the [SDK reference](https://pulseswap.io/docs/sdk).

## Limits & etiquette

- **Stay under 60 requests per minute** per IP. Responses carry no `RateLimit-*` headers today, so budget your own calls rather than waiting to be told; back off if you are refused, and honour `Retry-After` if one arrives.
- **Identify yourself** with a real `User-Agent` — a name and a URL we could reach you at. An unlabelled flood from a datacentre IP looks like the scrapers we block.
- **Do not scrape the HTML.** Every page of this documentation is served as Markdown at the same URL plus `.md`, which is cheaper for you to read and cannot break under a redesign.
- **CORS is open** — `Access-Control-Allow-Origin: *`, and preflight is answered — so a browser extension or a front end can call the API directly.

## Machine-readable docs

This documentation is built to be read by a program as well as a person, so an agent can find the rest of it without being handed each link:

- [`/llms.txt`](https://pulseswap.io/llms.txt) — an index of every page on the site, each entry pointing at its Markdown.
- **`/docs/<page>.md`** — any article as clean Markdown. This page is `/docs/agents.md`. Each page also declares its mirror as `rel="alternate"` in the HTML.
- **"Copy for your AI agent"** — the chip under the title of every page you build from puts that page on the clipboard with a short framing preamble. The one on the [docs home](https://pulseswap.io/docs) copies the whole integration guide at once.

Every endpoint, field and response on this page was probed against the live service rather than copied from a changelog, and the response above is a real capture with `tx.data` truncated.

---

Canonical HTML page: <https://pulseswap.io/docs/agents>
