empx-swap-sdk
v2.2.0
Published
Multi-chain DEX swap SDK — TypeScript-native, AI-agent-first, with dual on-chain + off-chain affiliate models
Maintainers
Readme
empx-swap-sdk
TypeScript-native multi-chain DEX swap SDK for EVM chains. It finds swap paths, builds transaction calldata, returns USD price quotes, supports affiliate routing, and exposes schema-friendly helpers for AI agent workflows.
Install
npm install empx-swap-sdkPackage Entrypoints
const sdk = require("empx-swap-sdk");
const wallet = require("empx-swap-sdk/wallet");
const agent = require("empx-swap-sdk/agent");ESM imports are also supported:
import { createRouter, CHAIN_IDS } from "empx-swap-sdk";
import { createBurnerWallet } from "empx-swap-sdk/wallet";The package ships dual CommonJS and ESM output with TypeScript declarations.
Quick Start
const { createRouter, CHAIN_IDS, getProtocolFeeBps } = require("empx-swap-sdk");
const router = createRouter(CHAIN_IDS.ARBITRUM);
console.log(getProtocolFeeBps()); // "28"
const tradeInfo = await router.getTradeInfo(
"1000000000000000000",
"0xTokenIn",
"0xTokenOut",
3,
200
);
const calldata = router.getSwapCalldata(tradeInfo, "0xRecipient");
// Give calldata to a wallet, backend signer, or transaction orchestration layer.
console.log(calldata);Core Flow
- Create a router for one chain with
createRouter(chainId, provider?, config?). - Call
getTradeInfo()to fetch a route, quote metadata, and slippage-adjusted output. - For ERC-20 input tokens, call
checkAllowance()and build approval calldata if needed. - Build swap calldata with
getSwapCalldata(),getSwapFromNativeCalldata(),getSwapToNativeCalldata(),prepareSwap(), or the legacyswap()alias. - Submit the returned
{ to, data, value }through your own wallet or signer.
Quotes expire after 30 seconds. Check tradeInfo.validUntil > Date.now() before
building calldata, and refetch if the quote is stale.
Supported Chains
| Chain | Chain ID | Native Token | | --- | ---: | --- | | PulseChain | 369 | PLS | | BSC | 56 | BNB | | Arbitrum | 42161 | ETH | | Base | 8453 | ETH | | Polygon | 137 | POL | | Avalanche | 43114 | AVAX | | Optimism | 10 | ETH | | Monad | 143 | MON | | Sonic | 146 | S | | Sei | 1329 | SEI | | Berachain | 80094 | BERA | | Rootstock | 30 | RBTC | | EthPOW | 10001 | ETHW | | HyperEVM | 999 | HYPE |
API Reference
createRouter(chainId, provider?, config?)
Returns a router instance bound to one chain.
const { createRouter, CHAIN_IDS } = require("empx-swap-sdk");
const defaultRouter = createRouter(CHAIN_IDS.ARBITRUM);
const rpcRouter = createRouter(CHAIN_IDS.BASE, "https://mainnet.base.org");
const providerRouter = createRouter(CHAIN_IDS.POLYGON, ethersProvider);
const signerRouter = createRouter(CHAIN_IDS.BSC, ethersSigner);
const fallbackRouter = createRouter(CHAIN_IDS.ARBITRUM, [
"https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY",
"https://arb1.arbitrum.io/rpc",
"https://arbitrum.llamarpc.com",
]);provider may be an RPC URL, an RPC URL array, an ethers provider, or an ethers
signer. RPC URL arrays use an ethers FallbackProvider with quorum: 1,
ordered priority, and a short stall timeout.
createRouters(chainIds, config?)
Creates routers for an explicit chain list and returns a record keyed by chain ID.
const { createRouters, CHAIN_IDS } = require("empx-swap-sdk");
const routers = createRouters([
CHAIN_IDS.ARBITRUM,
CHAIN_IDS.BASE,
CHAIN_IDS.BSC,
]);
const routersWithProviders = createRouters([
CHAIN_IDS.ARBITRUM,
CHAIN_IDS.BASE,
], {
providers: {
[CHAIN_IDS.ARBITRUM]: [
"https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY",
"https://arb1.arbitrum.io/rpc",
],
[CHAIN_IDS.BASE]: "https://mainnet.base.org",
},
});Use providers for batch usage because RPC URLs are usually chain-specific.
Batch validation rejects empty chain lists, duplicate chain IDs, invalid chain IDs,
provider overrides outside the requested chain list, and empty RPC fallback arrays.
getAllChainRouters(config?)
Convenience wrapper around createRouters(getSupportedChainIds(), config).
const { getAllChainRouters } = require("empx-swap-sdk");
const routers = getAllChainRouters();Use getAllChainRouters() only when the workflow truly needs every supported
chain. Prefer createRouters() for known subsets.
createAffiliateRouter(chainId, integratorId, provider?)
Creates a router that encodes the on-chain affiliate router ABI variant for every swap calldata call.
const { createAffiliateRouter, CHAIN_IDS } = require("empx-swap-sdk");
const router = createAffiliateRouter(
CHAIN_IDS.BASE,
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);integratorId must be a bytes32 hex string. Affiliate support is currently
available on PulseChain, Sonic, Base, and Monad.
To register as an integrator, contact EmpX with your protocol name and contact details.
- Telegram:
t.me/EmpXEmpseal - X:
@EmpXio
Router Methods
Path Finding
const result = await router.findBestPath(
"1000000000000000000",
"0xTokenIn",
"0xTokenOut",
3
);Use router.chain.NATIVE_ADDRESS for native input or output tokens.
Trade Info
const tradeInfo = await router.getTradeInfo(
"1000000000000000000",
"0xTokenIn",
"0xTokenOut",
3,
200
);tradeInfo includes amountIn, amountOut, fee, amounts, path,
adapters, quoteId, validUntil, and sdkVersion.
Allowance and Approval
const allowance = await router.checkAllowance(
"0xTokenIn",
"0xOwner",
tradeInfo.amountIn
);
if (!allowance.approved) {
const approval = router.getApprovalCalldata("0xTokenIn", tradeInfo.amountIn);
console.log(approval);
}getApprovalCalldata(tokenAddress) builds unlimited approval calldata.
getApprovalCalldata(tokenAddress, amount) builds exact-amount approval calldata.
Swap Calldata
const erc20ToErc20 = router.getSwapCalldata(tradeInfo, "0xRecipient");
const nativeToErc20 = router.getSwapFromNativeCalldata(tradeInfo, "0xRecipient");
const erc20ToNative = router.getSwapToNativeCalldata(tradeInfo, "0xRecipient");prepareSwap() fetches trade info and selects the correct calldata builder.
swap() is retained as the legacy alias for the same calldata-only behavior:
const { tradeInfo, calldata, swapType } = await router.prepareSwap(
amountIn,
tokenIn,
tokenOut,
"0xRecipient",
3,
200
);swapType is one of WrapNative, UnwrapNative, ERC20ToERC20,
NativeToERC20, or ERC20ToNative.
If the router was created with a signer, executeSwap() sends the transaction
and waits for the receipt:
const result = await router.executeSwap(amountIn, tokenIn, tokenOut, recipient);
console.log(result.hash);Wrap and Unwrap
const wrap = router.getWrapCalldata({ amountIn: "1000000000000000000" });
const unwrap = router.getUnwrapCalldata({ amountIn: "1000000000000000000" });USD Quotes
const price = await router.getTokenPriceUSD(router.chain.WRAPPED_NATIVE);
const quote = await router.getQuoteUSD(
"0xTokenAddress",
"5000000000000000000"
);
const prices = await router.getMultipleTokenPricesUSD([
"0xTokenA",
"0xTokenB",
router.chain.WRAPPED_NATIVE,
]);Token Helpers
const decimals = await router.getTokenDecimals("0xTokenAddress");
const symbol = await router.getTokenSymbol("0xTokenAddress");Affiliate Earnings
const earning = await router.estimateAffiliateEarning(
"0xTokenIn",
"1000000000000000000"
);Chain Helpers
const {
CHAIN_IDS,
CHAINS,
getChainConfig,
getAllChains,
getSupportedChainIds,
} = require("empx-swap-sdk");
const chain = getChainConfig(CHAIN_IDS.ARBITRUM);
const chains = getAllChains();
const chainIds = getSupportedChainIds();Each router also exposes router.chain, including chainId, name,
ROUTER_ADDRESS, NATIVE_ADDRESS, WRAPPED_NATIVE, USD_STABLE,
STABLE_TOKENS, and TRUSTED_TOKENS.
AI Agent Integration
The SDK is designed for deterministic, schema-defined, side-effect-controlled agent workflows. Agents should build calldata and return it to a signer unless the application explicitly grants transaction authority.
Standard Agent Swap Workflow
- Call
router.getTradeInfo(...). - If the input token is an ERC-20, call
router.checkAllowance(...). - If approval is needed, call
router.getApprovalCalldata(...)and return the approval calldata to the signer. - Check
tradeInfo.validUntil > Date.now(). - Call
router.getSwapCalldata(...)orrouter.swap(...). - Return calldata to the wallet, user, or signer.
Recommended agent response shape:
{
"chainId": 42161,
"swapType": "ERC20ToERC20",
"tradeInfo": {
"amountIn": "1000000000000000000",
"amountOut": "960400000000000000",
"fee": "28",
"quoteId": "a3f2c1d4-...",
"validUntil": 1712345678000,
"sdkVersion": "2.2.0"
},
"calldata": {
"to": "0x...",
"data": "0x...",
"value": "0"
}
}Tool Schemas
const {
TOOL_SCHEMAS,
getOpenAITools,
getClaudeTools,
getLangChainSchemas,
} = require("empx-swap-sdk");
const openAiTools = getOpenAITools();
const claudeTools = getClaudeTools();
const langChainSchemas = getLangChainSchemas();
console.log(TOOL_SCHEMAS.getTradeInfo);
console.log(TOOL_SCHEMAS.createRouters);
console.log(TOOL_SCHEMAS.getAllChainRouters);Use createRouters() for agent workflows that inspect a known set of networks.
Use getAllChainRouters() only for broad discovery or full network scans.
Wallet Helpers
const { createRouter, CHAIN_IDS } = require("empx-swap-sdk");
const {
createBurnerWallet,
fromPrivateKey,
fromMnemonic,
readOnly,
describeWallet,
getNativeBalance,
} = require("empx-swap-sdk/wallet");
const wallet = createBurnerWallet({ rpcUrl: "https://arb1.arbitrum.io/rpc" });
const router = createRouter(CHAIN_IDS.ARBITRUM, wallet.signer);Browser wallet helpers are also exported: connectMetaMask, connectRabby,
connectInjected, connectPrivy, and connectWagmi.
EIP-6963 Wallet Discovery
const {
discoverInjectedProviders,
connectViaEip6963,
KNOWN_WALLET_RDNS,
} = require("empx-swap-sdk");
const wallets = await discoverInjectedProviders();
const rabby = await connectViaEip6963(KNOWN_WALLET_RDNS.RABBY);connectMetaMask() and connectRabby() prefer EIP-6963 discovery and fall
back to legacy window.ethereum behavior.
EIP-5792 Wallet Calls
const {
calldataToWalletCall,
getWalletCapabilities,
sendWalletCalls,
} = require("empx-swap-sdk");
const approval = router.getApprovalCalldataForAmount(tokenIn, {
mode: "exact",
amount: amountIn,
});
const prepared = await router.prepareSwap(amountIn, tokenIn, tokenOut, recipient);
await getWalletCapabilities(provider, wallet.address, "0xa4b1");
await sendWalletCalls(provider, {
version: "2.0.0",
chainId: "0xa4b1",
from: wallet.address,
calls: [
calldataToWalletCall(approval),
calldataToWalletCall(prepared.calldata),
],
});Polished Wallet Execution Planning
For wallet integrations, prepareWalletSwap() chooses the safest available
flow in this order: already-approved swap, ERC-2612 permit, EIP-5792 batched
exact approval + swap, then exact approval followed by swap.
const {
prepareWalletSwap,
createRouter,
CHAIN_IDS,
} = require("empx-swap-sdk");
const router = createRouter(CHAIN_IDS.ARBITRUM, rpcProvider);
const plan = await prepareWalletSwap({
router,
account: wallet.address,
amountIn,
tokenIn,
tokenOut,
recipient,
preferPermit: true,
permit: {
signer,
tokenName,
tokenVersion: "1",
nonce,
deadline,
},
preferBatch: true,
eip1193Provider: browserProvider,
});
if (plan.strategy === "permit") {
await signer.sendTransaction(plan.swap);
} else if (plan.strategy === "batch") {
await browserProvider.request({
method: "wallet_sendCalls",
params: [{
version: "2.0.0",
chainId: `0x${router.chain.chainId.toString(16)}`,
from: wallet.address,
calls: plan.walletCalls,
}],
});
} else if (plan.strategy === "approval-then-swap") {
await signer.sendTransaction(plan.approval);
await signer.sendTransaction(plan.swap);
} else {
await signer.sendTransaction(plan.swap);
}x402 RPC Provider
const {
createRouter,
CHAIN_IDS,
createX402Provider,
PRESET_X402_ENDPOINTS,
} = require("empx-swap-sdk");
const provider = createX402Provider({
endpoint: PRESET_X402_ENDPOINTS.QUICKNODE,
paymentSigner,
});
const router = createRouter(CHAIN_IDS.BASE, provider);Use x402 when your RPC provider requires signed payment headers.
Advanced Fees
const {
getProtocolFeeBps,
setProtocolFeeBps,
makeAffiliateConfig,
classifyAffiliateTier,
enablePairTypeFees,
disablePairTypeFees,
} = require("empx-swap-sdk");Protocol fee and affiliate helpers are exported for integrations that need explicit fee reporting or tier classification. Pair-type fees are opt-in.
For server-side multi-tenant integrations, prefer instance-scoped config:
const router = createRouter(CHAIN_IDS.ARBITRUM, provider, {
protocolFeeBps: 28,
pairTypeFees: {
volatileVolatileBps: 28,
volatileStableBps: 15,
stableStableBps: 9,
},
});The global fee setters are retained for compatibility, but per-router config avoids crosstalk between tenants.
Permit and Approval Helpers
const {
buildPermitTypedData,
splitPermitSignature,
} = require("empx-swap-sdk");
const typed = buildPermitTypedData({
tokenName,
tokenVersion: "1",
chainId,
verifyingContract: tokenIn,
owner,
spender: router.chain.ROUTER_ADDRESS,
value: amountIn,
nonce,
deadline,
});
const signature = await signer.signTypedData(
typed.domain,
typed.types,
typed.message
);
const permit = splitPermitSignature(signature, deadline);
const calldata = router.getSwapWithPermitCalldata(tradeInfo, recipient, permit);For ordinary approvals, getApprovalCalldata(token) still builds unlimited
approval for compatibility. Agent integrations should prefer:
const approval = router.getApprovalCalldataForAmount(tokenIn, {
mode: "exact",
amount: amountIn,
});Viem and Wagmi Adapters
const { toViemTransaction } = require("empx-swap-sdk/adapters/viem");
const { toWagmiTransaction } = require("empx-swap-sdk/adapters/wagmi");
const prepared = await router.prepareSwap(amountIn, tokenIn, tokenOut, recipient);
const viemTx = toViemTransaction(prepared.calldata);
const wagmiTx = toWagmiTransaction(prepared.calldata);Split Routing
Split routing is available on every existing router object; there is no new
factory or separate router type. This works with createRouter(),
createAffiliateRouter(), createRouters(), and getAllChainRouters().
const { createRouter, CHAIN_IDS } = require("empx-swap-sdk");
const router = createRouter(CHAIN_IDS.BASE, provider);
const prepared = await router.splitSwap(
amountIn,
tokenIn,
tokenOut,
recipient,
{
routing: "auto",
maxSteps: 3,
slippageBps: 200,
maxSplits: 3,
minSavingsBps: 10,
}
);
console.log(prepared.routing); // "split" or "single"
console.log(prepared.calldata); // { to, data, value }| routing | Behavior |
| --- | --- |
| "auto" | Uses an atomic split only when deployed, compatible, and beneficial; otherwise returns the existing single-route result. This is the default. |
| "split" | Requires a beneficial split; throws SPLIT_UNAVAILABLE or SPLIT_NOT_BENEFICIAL instead of falling back. |
| "single" | Calls the existing router.swap() flow unchanged. |
When prepared.routing === "split", ERC-20 approval must target the multicall
router. For a single result, keep using the ordinary approval methods:
if (prepared.routing === "split") {
const allowance = await router.checkSplitAllowance(
tokenIn,
signer.address,
amountIn
);
if (!allowance.approved) {
const approval = router.getSplitApprovalCalldataForAmount(tokenIn, {
mode: "exact",
amount: amountIn,
});
await (await signer.sendTransaction(approval)).wait();
}
} else {
const allowance = await router.checkAllowance(tokenIn, signer.address, amountIn);
if (!allowance.approved) {
const approval = router.getApprovalCalldataForAmount(tokenIn, {
mode: "exact",
amount: amountIn,
});
await (await signer.sendTransaction(approval)).wait();
}
}
const receipt = await (await signer.sendTransaction(prepared.calldata)).wait();Agent and calldata-only integrations should return prepared.calldata and the
prepared.routing discriminator to the wallet/execution layer instead of
sending the transaction inside the agent.
Batch factories need no special handling:
const routers = createRouters([CHAIN_IDS.BASE, CHAIN_IDS.ARBITRUM], {
defaultProvider: provider,
});
const prepared = await routers[CHAIN_IDS.ARBITRUM].splitSwap(
amountIn,
tokenIn,
tokenOut,
recipient
);Current multicall deployments:
| Chain | Chain ID | Multicall router |
| --- | ---: | --- |
| Base | 8453 | 0x6B2e4876f4d0D0fb437Ce52c4B67ecde2bAC80ee |
| Arbitrum | 42161 | 0xFCc2Dd827E5E398a3288f31f7fF2e4124199001a |
| Optimism | 10 | 0xe5E108A8ADF9cB5fb1CF1642C2E1E1980CA1A2BC |
The current deployments are treated as V1. They support the standard
ERC-20-to-ERC-20 split path; native-input and native-output requests fall back
in auto mode and are rejected in forced split mode.
createAffiliateRouter() and createRouter(..., { integratorId }) also expose
splitSwap(). On the current V1 multicall routers, auto returns the existing
integrator-aware single route because V1 cannot carry integratorId. A future
router can be activated by replacing only MULTICALL_ROUTER_ADDRESS, provided
it retains multiSwap(...) and implements both exact V2 signatures:
function multicallVersion() external pure returns (uint256); // returns 2
function multiSwapWithIntegrator(MulticallLeg[] calldata legs, bytes32 integratorId) external payable;The SDK detects V2 per provider/address, uses the integrator-aware batch entry
point automatically, propagates RPC detection failures, and rejects unknown
versions. Advanced consumers can still import findBestSplitRouting(),
buildSplitMultiSwapCalldata(), and
buildSplitMultiSwapWithIntegratorCalldata() directly.
Full Example: Execute With ethers.js
This example is for wallet or backend signer integrations. Agent-only workflows should return calldata instead of sending transactions directly.
const { ethers } = require("ethers");
const { createRouter, CHAIN_IDS } = require("empx-swap-sdk");
const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const router = createRouter(CHAIN_IDS.ARBITRUM, provider);
async function executeSwap(tokenIn, tokenOut, amountIn, recipient) {
const isNative = tokenIn === router.chain.NATIVE_ADDRESS;
if (!isNative) {
const allowance = await router.checkAllowance(tokenIn, signer.address, amountIn);
if (!allowance.approved) {
const approval = router.getApprovalCalldata(tokenIn, amountIn);
const approveTx = await signer.sendTransaction(approval);
await approveTx.wait();
}
}
const { calldata } = await router.swap(
amountIn,
tokenIn,
tokenOut,
recipient,
3,
200
);
const swapTx = await signer.sendTransaction(calldata);
return swapTx.wait();
}Structured Errors
All SDK errors use EmpxError with a machine-readable code, retryable flag,
and optional context.
const { EmpxError } = require("empx-swap-sdk");
try {
await router.getTradeInfo(amountIn, tokenIn, tokenOut);
} catch (err) {
if (err instanceof EmpxError) {
console.log(err.code);
console.log(err.retryable);
console.log(err.toJSON());
}
}| Code | Retryable | Meaning |
| --- | --- | --- |
| INVALID_INPUT | No | Missing or malformed parameter |
| INVALID_ADDRESS | No | Invalid EVM address |
| INVALID_AMOUNT | No | Zero or invalid integer amount |
| SLIPPAGE_TOO_HIGH | No | Slippage exceeds 1000 bps |
| STEPS_OUT_OF_RANGE | No | maxSteps must be 1 through 4 |
| AMOUNT_TOO_SMALL | No | Input too small after protocol fee |
| NO_ROUTE_FOUND | Yes | No route found, possibly transient RPC state |
| QUOTE_EXPIRED | Yes | Quote TTL elapsed and trade info must be refetched |
| SPLIT_UNAVAILABLE | No | Forced split is not deployed or compatible for this request |
| SPLIT_NOT_BENEFICIAL | No | No candidate met the forced split savings threshold |
Retry and Rate Limits
- Retry only errors with
retryable: true. - Use short exponential backoff such as 500 ms, 1 s, then 2 s.
- Cap retries to avoid loops.
- Use private or dedicated RPC endpoints for production agents.
- Prefer
getMultipleTokenPricesUSD()over callinggetTokenPriceUSD()in a loop.
Testing
npm test # Smoke test, no RPC
npm run test:fast # Smoke + unit tests, no RPC
npm run test:unit # Unit tests only
npm run test:pathfind # Pathfinding and quotes across selected chains
npm run test:affiliate # Affiliate model tests
npm run test:split # Split routing tests
npm run test:all # Full suite, requires RPC accessLicense
MIT
