# Bot trading: buy and sell

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](/docs/network/) 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.

```sh
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](/docs/abi/RearctorPool.json), [factory ABI](/docs/abi/RearctorLaunchpad.json) and [quoter ABI](/docs/abi/V4Quoter.json) 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.

```js
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 `Trade` and V4 `Swap` records 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](/docs/api-reference/) for response schemas and [integrations](/docs/integrations/) for ABI downloads and event-indexing conventions.
