PulseSwap for bots & AI agents
One click copies the whole page as agent instructions — endpoints, addresses and code the agent can trust verbatim, plus a pointer to the rest of the docs.
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.
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.
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" } }'
{
"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"
}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
0x0000000000000000000000000000000000000000infromTokenortoToken. Sending it in puts the amount intx.valueinstead of needing an approval. amountInis a wei string, never a number — a large value does not survive a JavaScript number.slippageis a percent between 0.0 and 100.0:0.5means 0.5%.tx.tois 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:
- Quote without
userAddress— to price a pair, watch a spread, or decide whether to act at all. You getamountOutandgasEstimate; thetxthat comes back carries the zero address and is not meant to be sent. - Quote again with
userAddressonce you have decided. Same call, and nowtxis built for your account. - Approve if needed, then sign and send. An ERC-20 going in needs one approval with the router as spender; after that you sign
txwith your own key and broadcast it.
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),
});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) is 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.
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.
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.
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 honourRetry-Afterif 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— 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 asrel="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 copies the whole integration guide at once.
tx.data truncated.