Build on ApexisRouter
One contract in front of every DEX on Arc. Send it a swap and it routes to the pool you name, takes the protocol fee inside the same transaction, and pays your wallet directly. No API key, no allowlist, no off-chain signature.
Contract addresses
Chain ID 5042 · compiler v0.8.26+commit.8a97fa7a · fee 30 bps. The router address can rotate; read it from the JSON endpoint at startup rather than hardcoding it.
ApexisRouter (fee router — call this)
0xed32e78cdcd587c90fa104e38209b4512d6d5fd5
Approve your tokens to THIS address, not to the DEX.
Retired revisions still on chain: 0xb0d65ccafa28385f81626ff6a7db05abf0959d1c — do not send new swaps to them.
Registered DEXes
Pass one of these as the router argument. Each DEX has its own WUSDC; the buy/sell paths must start or end with the WUSDC of the DEX you name.
| DEX | Router | WUSDC | Flavour | Status |
|---|---|---|---|---|
| APEXISWAPid 1 | 0x1229377adE15278818cD50214456db4b469f6f2b | 0x1CCA7CA62bcBFdAB086765f451648d8C7591AECb | ETH_STYLE | enabled |
| Uniswapid 2 | 0x1f7d7550b1b028f7571e69a784071f0205fd2efa | 0x3600000000000000000000000000000000000000 | NATIVE_ERC20 | enabled |
How the fee works
Standard
30 bps
Taken on the USDC side of the trade, inside the swap. Max the owner can ever set: 300 bps. Default in the contract: 30 bps.
Graduated Pump token
100 bps
Instead of the standard rate, not on top. 80% of it accrues to the token creator. quoteFeeForPath returns the right rate for you.
Wrap / unwrap
0 bps
Native USDC to WUSDC 1:1 is not a swap. Call the WUSDC contract directly; the router is not involved.
Quote with the net amount. On a buy the fee leaves before the DEX sees your funds, so getAmountsOut(gross) promises more than arrives and the swap reverts with INSUFFICIENT_OUTPUT. Call quoteFeeForPath first and quote the DEX with net. On a sell, subtract the fee from the DEX output before setting minOut.
Functions you will call
quoteFee(uint256 amountIn)→ (uint256 fee, uint256 net)Fee split at the standard rate. Quote the DEX with `net`, never the gross.
quoteFeeForPath(address[] path, uint256 amountIn)→ (uint256 fee, uint256 net, uint16 bps)Same, for a specific route. Returns the graduated rate when the path contains a graduated Apexis Pump token. Use this one.
swapNativeForTokens(address router, address[] path, uint256 minOut, uint256 deadline)→ payableBUY with native USDC. `path[0]` must be that DEX’s WUSDC. Fee skimmed from `msg.value`; the DEX pays you directly.
swapTokensForNative(address router, address[] path, uint256 amountIn, uint256 minOut, uint256 deadline)SELL to native USDC. Last path element must be that DEX’s WUSDC. Fee taken from the output; `minOut` is checked on what you receive.
swapTokensForTokens(address router, address[] path, uint256 amountIn, uint256 minOut, uint256 deadline)Token to token on one DEX. Fee is taken on the WUSDC leg if there is one, otherwise skimmed from the input.
swapTokensForTokensCrossDex((address routerA, address routerB, uint256 amountIn, uint256 minOutFinal, uint256 deadline) p, address[] pathA, address[] pathB)Sell on DEX A into native, buy on DEX B. One fee on the native middle. Disabled unless `crossDexEnabled` is true.
swapInProgress(address token)→ boolTrue only while this router is mid-swap on a path containing `token`. Graduated Pump tokens use it to gate pair transfers.
claimCreatorFees(address creator, address token)Pays out the creator share accrued on a graduated token. Anyone may call; funds go to `creator`.
Examples
Addresses below are the live ones. Amounts use 18 decimals because native USDC on Arc has 18 decimals; ERC-20 tokens launched on Apexis Pump also use 18.
Read the live config
// The address can rotate. Read it at startup instead of hardcoding it.
const res = await fetch('https://www.apexiswap.com/api/developers/router');
const { address, abi, feeBps, dexes } = await res.json();
const enabledDexes = dexes.filter((d) => d.enabled);
// -> [{ id: 1, name: 'APEXISWAP', router: '0x…', wusdc: '0x…', flavour: 'ETH_STYLE', enabled: true }, …]Buy a token with native USDC (ethers v6)
import { ethers } from 'ethers';
const ROUTER = '0xed32e78cdcd587c90fa104e38209b4512d6d5fd5'; // ApexisRouter
const DEX_ROUTER = '0x1229377adE15278818cD50214456db4b469f6f2b'; // APEXISWAP
const WUSDC = '0x1CCA7CA62bcBFdAB086765f451648d8C7591AECb'; // APEXISWAP's wrapped USDC
const TOKEN = '0x0000000000000000000000000000000000000000'; // APXS
// Only the pieces you call. The full ABI is at https://www.apexiswap.com/api/developers/router
const abi = [
'function quoteFeeForPath(address[] path, uint256 amountIn) view returns (uint256 fee, uint256 net, uint16 bps)',
'function swapNativeForTokens(address router, address[] path, uint256 minOut, uint256 deadline) payable',
];
const dexAbi = ['function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)'];
const provider = new ethers.JsonRpcProvider('https://rpc.mainnet.arc.io', 5042);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const router = new ethers.Contract(ROUTER, abi, wallet);
const dex = new ethers.Contract(DEX_ROUTER, dexAbi, provider);
const path = [WUSDC, TOKEN];
const amountIn = ethers.parseUnits('10', 18); // native USDC on Arc has 18 decimals
// 1. Ask the router what reaches the pool after its fee. Quote the DEX with NET,
// not gross: quoting the gross promises more than arrives and the swap reverts.
const { net, bps } = await router.quoteFeeForPath(path, amountIn);
console.log('fee bps for this path:', bps); // 30 normally, 100 for a graduated Pump token
// 2. Quote the DEX with the net amount and apply your slippage tolerance.
const amounts = await dex.getAmountsOut(net, path);
const expectedOut = amounts[amounts.length - 1];
const minOut = (expectedOut * 995n) / 1000n; // 0.5% slippage
// 3. Swap. The fee is skimmed from msg.value; tokens go straight to your wallet.
const deadline = Math.floor(Date.now() / 1000) + 600;
const tx = await router.swapNativeForTokens(DEX_ROUTER, path, minOut, deadline, { value: amountIn });
console.log('sent', tx.hash);
await tx.wait();Sell a token for native USDC (ethers v6)
import { ethers } from 'ethers';
const ROUTER = '0xed32e78cdcd587c90fa104e38209b4512d6d5fd5';
const DEX_ROUTER = '0x1229377adE15278818cD50214456db4b469f6f2b';
const WUSDC = '0x1CCA7CA62bcBFdAB086765f451648d8C7591AECb';
const TOKEN = '0x0000000000000000000000000000000000000000';
const abi = [
'function swapTokensForNative(address router, address[] path, uint256 amountIn, uint256 minOut, uint256 deadline)',
];
const erc20 = ['function approve(address spender, uint256 value) returns (bool)'];
const dexAbi = ['function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)'];
const provider = new ethers.JsonRpcProvider('https://rpc.mainnet.arc.io', 5042);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const router = new ethers.Contract(ROUTER, abi, wallet);
const token = new ethers.Contract(TOKEN, erc20, wallet);
const dex = new ethers.Contract(DEX_ROUTER, dexAbi, provider);
const path = [TOKEN, WUSDC];
const amountIn = ethers.parseUnits('1000', 18);
// Approve the FEE ROUTER, not the DEX. The router pulls your tokens, and the DEX
// pays the router so the fee can be taken on the USDC side.
await (await token.approve(ROUTER, amountIn)).wait();
// On a sell the fee comes off the OUTPUT, so quote the DEX with the full input and
// then subtract the fee from the expected output before setting minOut.
const amounts = await dex.getAmountsOut(amountIn, path);
const grossOut = amounts[amounts.length - 1];
const netOut = (grossOut * (10000n - 30n)) / 10000n; // 30 bps, or 100 for a graduated token
const minOut = (netOut * 995n) / 1000n;
const deadline = Math.floor(Date.now() / 1000) + 600;
// minOut is checked against what YOU receive, after the fee.
const tx = await router.swapTokensForNative(DEX_ROUTER, path, amountIn, minOut, deadline);
await tx.wait();Same buy with viem
import { createPublicClient, createWalletClient, http, parseAbi, parseUnits } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const arc = {
id: 5042,
name: 'Arc',
nativeCurrency: { name: 'USDC', symbol: 'USDC', decimals: 18 },
rpcUrls: { default: { http: ['https://rpc.mainnet.arc.io'] } },
} as const;
const ROUTER = '0xed32e78cdcd587c90fa104e38209b4512d6d5fd5' as const;
const DEX_ROUTER = '0x1229377adE15278818cD50214456db4b469f6f2b' as const;
const WUSDC = '0x1CCA7CA62bcBFdAB086765f451648d8C7591AECb' as const;
const TOKEN = '0x0000000000000000000000000000000000000000' as const;
const routerAbi = parseAbi([
'function quoteFeeForPath(address[] path, uint256 amountIn) view returns (uint256 fee, uint256 net, uint16 bps)',
'function swapNativeForTokens(address router, address[] path, uint256 minOut, uint256 deadline) payable',
]);
const dexAbi = parseAbi(['function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[])']);
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: arc, transport: http() });
const walletClient = createWalletClient({ account, chain: arc, transport: http() });
const path = [WUSDC, TOKEN] as const;
const amountIn = parseUnits('10', 18);
const [, net] = await publicClient.readContract({
address: ROUTER, abi: routerAbi, functionName: 'quoteFeeForPath', args: [path, amountIn],
});
const amounts = await publicClient.readContract({
address: DEX_ROUTER, abi: dexAbi, functionName: 'getAmountsOut', args: [net, path],
});
const minOut = (amounts[amounts.length - 1] * 995n) / 1000n;
const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);
const hash = await walletClient.writeContract({
address: ROUTER, abi: routerAbi, functionName: 'swapNativeForTokens',
args: [DEX_ROUTER, path, minOut, deadline], value: amountIn,
});
await publicClient.waitForTransactionReceipt({ hash });Listen for swaps
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://rpc.mainnet.arc.io', 5042);
const router = new ethers.Contract('0xed32e78cdcd587c90fa104e38209b4512d6d5fd5', [
'event SwapExecuted(address indexed user, address indexed router, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, address feeToken, uint256 feeAmount)',
], provider);
// amountOut is the recipient's measured balance delta, so it is truthful even for
// fee-on-transfer tokens. feeToken is address(0) when the fee was taken in native USDC.
router.on('SwapExecuted', (user, dexRouter, tokenIn, tokenOut, amountIn, amountOut, feeToken, feeAmount) => {
console.log({ user, dexRouter, tokenIn, tokenOut, amountIn, amountOut, feeToken, feeAmount });
});Revert reasons
The router uses short string reasons. If your call reverts with missing revert data and no reason, it is almost always the public RPC rate-limiting you (HTTP 429) rather than the contract — retry with backoff.
| UNKNOWN_ROUTER | The `router` argument is not a registered DEX. Use one from the table above. |
| ROUTER_DISABLED | That DEX is registered but switched off by the operator. |
| PATH_NOT_WUSDC_IN | `swapNativeForTokens`: `path[0]` is not that DEX’s WUSDC. Each DEX has its own. |
| PATH_NOT_WUSDC_OUT | `swapTokensForNative`: last path element is not that DEX’s WUSDC. |
| BAD_PATH | Path shorter than 2 or with a zero address. |
| SAME_TOKEN | First and last path elements are the same token. |
| INSUFFICIENT_OUTPUT | What you would receive, after the fee, is below `minOut`. Re-quote or widen slippage. |
| NOTHING_PULLED | `transferFrom` moved zero tokens. Check the approval is to the router, not the DEX. |
| NO_VALUE / NO_AMOUNT | Zero `msg.value` or zero `amountIn`. |
| REENTRANT | Re-entered during a swap. Do not call the router from a token hook. |
Prices, pools and history
For quotes you should always ask the DEX router on chain. For everything else — pair lists, reserves, hourly candles, recent swaps, token metadata, Apexis Pump curves — use the public REST API. It is indexed from the same chain and needs no key.
Open the REST API reference