A bot can trade through the same Rearctor pool before and after graduation. Use the read API to find tokens, then send signed transactions to the canonical pool. There is no REST POST /buy or POST /sell, no trading API key, and no need to automate the website.
Quick reference#
| Operation | Interface |
|---|---|
| Find a token | GET /tokens?q=REARC&limit=10 |
| Resolve exact identity | GET /tokens/{tokenAddress} or factory launch ID |
| Curve quote | Pool quoteBuy(amount) / quoteSell(amount) |
| Graduated quote | V4 quoter quoteExactInputSingle via RPC simulation |
| Approve input | Input ERC-20 approve(pool, amount) |
| Buy, either stage | Pool buy(amount, minOut, deadline) |
| Sell, either stage | Pool sell(amount, minOut, deadline) |
| Execution confirmation | RPC transaction receipt and pool Trade event |
| Indexed history | GET /tokens/{tokenAddress}/history |
The spender and transaction destination are the factory-registered Rearctor pool, not the token, fee vault, hook or a generic Uniswap router. After graduation the pool routes into its canonical Uniswap V4 market with the Rearctor hook. The bought tokens or sale proceeds go to the calling wallet. These methods do not accept a separate recipient.
Choose the network and deployment#
Use https://api.rearctor.io/api/testnet for Arc Testnet, chain ID 5042002, or https://api.rearctor.io/api for Arc Mainnet, chain ID 5042. Obtain the factory and V4 quoter from the deployment information for your selected deployment. Pin verified addresses in your bot configuration rather than trusting addresses returned by a search. A mainnet API response in demo mode is not tradable.
curl --fail --show-error \
'https://api.rearctor.io/api/testnet/tokens?q=REARC&limit=10'
Check status.mode === "live", status.chainId, status.factory and snapshot freshness. An empty catalogue or /official returning upcoming means there is no verified official launch available from that index yet. Numeric IDs are scoped to one factory and network; names and tickers are not unique identities.
Amounts, quotes and fees#
Buy input is ERC-20 USDC with 6 decimals: 10 USDC is 10000000n. Sell input is the launch token with 18 decimals: 100 tokens is 100000000000000000000n. Use parseUnits and BigInt throughout.
Before graduation, quoteBuy and quoteSell return [amountOut, fee]. After graduation, use the V4 quoter with the exact getPoolKey() returned by the pool. zeroForOne is true when the input asset equals currency0. Keep hookData: '0x'. The quoter returns [amountOut, gasEstimate]; its second value is not a fee and is not the full transaction gas limit. A quoter simulation uses eth_call; do not broadcast the quoter call.
Quote output already reflects the trading fee. Do not subtract the fee again. Curve fees use USDC; graduated buy fees use USDC and graduated sell fees use the input token. The indexed display price is not an executable quote. Read graduated on-chain every time: an index can still show a curve while migration has already completed.
JavaScript example with viem#
Install viem in your bot project. Download the published pool ABI, factory ABI and quoter ABI into a local abi directory, and pin them to the deployment you have verified. ABI updates alone do not prove that deployed bytecode has changed.
The following Node.js module exports a reusable function. Pass configured viem public/wallet clients and a local account or external signer; never send a signing key to the Rearctor API. execute defaults to false and only returns a quote. Setting it to true authorizes approval and trade transactions from that signer.
import { readFileSync } from 'node:fs';
import { erc20Abi, parseUnits } from 'viem';
const abi = name => JSON.parse(readFileSync(
new URL(`./abi/${name}.json`, import.meta.url), 'utf8'));
const poolAbi = abi('RearctorPool');
const factoryAbi = abi('RearctorLaunchpad');
const quoterAbi = abi('V4Quoter');
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
export async function trade({
client, wallet, account, apiBase, chainId,
factory, quoter, token, side, amount,
slippageBps = 100, execute = false,
}) {
if (!['buy', 'sell'].includes(side)) throw Error('Invalid side');
if (!Number.isInteger(slippageBps) || slippageBps < 10 || slippageBps > 1000)
throw Error('Slippage must be 10–1000 basis points');
if (await client.getChainId() !== chainId || await wallet.getChainId() !== chainId)
throw Error('Wrong network');
const input = parseUnits(amount, side === 'buy' ? 6 : 18);
if (input <= 0n || input >= 2n ** 127n) throw Error('Invalid amount');
const response = await fetch(`${apiBase}/tokens/${token}`, {
signal: AbortSignal.timeout(15000),
});
if (!response.ok) throw Error(`Index HTTP ${response.status}`);
const { launch, status } = await response.json();
if (status.mode !== 'live' || status.chainId !== chainId ||
!status.factory || !same(status.factory, factory) ||
status.state !== 'ready' || !status.updatedAt ||
Date.now() - status.updatedAt > 120000)
throw Error('Wrong deployment or index not current');
const pool = launch.pool;
async function quote() {
const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
const read = (address, abi, functionName, args = []) =>
client.readContract({ address, abi, functionName, args, blockNumber });
const p = name => read(pool, poolAbi, name);
const f = (name, args) => read(factory, factoryAbi, name, args);
const [registered, origin, actualToken, quoteToken, vault, hook, manager,
factoryHook, factoryManager, factoryQuote, key, graduated] = await Promise.all([
f('isPool', [pool]), p('factory'), p('token'), p('quote'), p('vault'),
p('hook'), p('manager'), f('hook'), f('manager'), f('quoteToken'),
p('getPoolKey'), p('graduated'),
]);
const [currency0, currency1] = BigInt(actualToken) < BigInt(quoteToken)
? [actualToken, quoteToken] : [quoteToken, actualToken];
if (!registered || !same(origin, factory) || !same(actualToken, token) ||
!same(actualToken, launch.token) || !same(quoteToken, launch.quote) ||
!same(vault, launch.vault) || !same(hook, factoryHook) ||
!same(manager, factoryManager) || !same(quoteToken, factoryQuote) ||
!same(key.currency0, currency0) || !same(key.currency1, currency1) ||
!same(key.hooks, hook) || Number(key.fee) !== 0x800000 ||
Number(key.tickSpacing) !== 200)
throw Error('Not the canonical Rearctor market');
const inputToken = side === 'buy' ? quoteToken : actualToken;
let out;
if (!graduated) {
[out] = await read(pool, poolAbi,
side === 'buy' ? 'quoteBuy' : 'quoteSell', [input]);
} else {
if (!quoter || !same(await read(quoter, quoterAbi, 'poolManager'), manager))
throw Error('Wrong V4 quoter');
const { result } = await client.simulateContract({
address: quoter, abi: quoterAbi, functionName: 'quoteExactInputSingle',
blockNumber, args: [{ poolKey: key,
zeroForOne: same(inputToken, key.currency0),
exactAmount: input, hookData: '0x' }],
});
[out] = result;
}
if (out <= 0n) throw Error('No executable output');
return { out, inputToken, graduated, blockNumber };
}
const initial = await quote();
const minimum = out => out * BigInt(10000 - slippageBps) / 10000n;
const originalMin = minimum(initial.out);
if (originalMin <= 0n) throw Error('Minimum output rounds to zero');
if (!execute) return { pool, input, minOut: originalMin, ...initial };
async function send(address, abi, functionName, args, mayMigrate = false) {
const { request } = await client.simulateContract({
account, address, abi, functionName, args,
});
const estimate = await client.estimateContractGas(request);
const gas = (estimate * 120n + 99n) / 100n +
(mayMigrate ? 1000000n : 100000n);
const hash = await wallet.writeContract({ ...request, gas });
// Persist hash here before waiting, to recover safely after a restart.
console.log('Submitted', hash);
const receipt = await client.waitForTransactionReceipt({ hash });
if (receipt.status !== 'success') throw Error(`Reverted: ${hash}`);
return receipt;
}
const owner = typeof account === 'string' ? account : account.address;
const allowance = await client.readContract({
address: initial.inputToken, abi: erc20Abi, functionName: 'allowance',
args: [owner, pool],
});
if (allowance < input) {
if (allowance > 0n)
await send(initial.inputToken, erc20Abi, 'approve', [pool, 0n]);
await send(initial.inputToken, erc20Abi, 'approve', [pool, input]);
}
const fresh = await quote();
if (fresh.out < originalMin) throw Error('Price moved: trade not submitted');
const minOut = minimum(fresh.out) > originalMin
? minimum(fresh.out) : originalMin;
const deadline = BigInt(Math.floor(Date.now() / 1000) + 120);
return send(pool, poolAbi, side, [input, minOut, deadline],
side === 'buy' && !fresh.graduated);
}
Call with side: 'buy', amount: '10' to quote a 10 USDC purchase, or side: 'sell', amount: '100' to quote selling 100 tokens. All addresses, network settings and the signer are explicit inputs. Add execute: true only when your bot's execution policy permits the trade. The example is a single-trade building block, not a strategy, nonce scheduler or persistent transaction service.
Graduation and execution handling#
A curve buy reaching the 5,042 USDC threshold can migrate liquidity in the same transaction. Its actual USDC consumption can be less than the input maximum; use receipt events and balance changes for accounting. Include migration gas headroom even when a quote is below the threshold: earlier transactions can move the pool before yours executes. After migration the public buy and sell calls keep working at the same pool address.
Arc gas uses native USDC with 18 decimals, while approvals and buy amounts use the 6-decimal ERC-20 interface. They share the underlying balance. Keep USDC available for approval and trade gas; do not spend the full ERC-20 balance on a buy. Estimate costs for each pending transaction and enforce your own spending and gas limits.
Running a reliable bot#
- Serialize transactions per signer or implement a persistent nonce queue. Save each hash before waiting for its receipt. A timeout does not mean the transaction failed: check the hash and nonce before retrying, otherwise you can buy twice.
- Requote after approvals. Preserve the original minimum output; do not silently widen slippage. Graduation can occur between reads, so simulation may fail and require a fresh decision.
- Wait for the next block on
LaunchBlockRestricted. On expiry or slippage errors, request a new quote and deadline. Do not blindly replay the old transaction. - Poll catalogue data at a modest interval, such as ten seconds. Back off on HTTP 429/503, respect
Retry-After, and use bounded RPC retries. There is no public streaming API documented here. - Use receipts for immediate execution confirmation, then reconcile confirmed history through the read API. Deduplicate by chain, transaction hash and log index; curve
Tradeand V4Swaprecords can represent the same execution. - Maintain balances, maximum position size, minimum received, gas budget and receipt confirmations in your own bot. A successful simulation is not a promise of later execution at the same state.
See the API reference for response schemas and integrations for ABI downloads and event-indexing conventions.