routr

Docs

Build on routr.

Platforms, pools, the two messages, and every method, view, event and error.

Overview

routr is one NEAR contract that holds two registries: platforms (a launchpad, a wallet, any front end that opens markets) and pools, one per token, quote and platform. A pool is a constant-product market with a virtual quote reserve: the token side is seeded once, there are no LP shares and no liquidity withdrawal, and the quote side starts from a virtual reserve that, with the seed, sets the opening price. Swaps are a single ft_transfer_call. Fees are booked per account at swap time and paid out by anyone.

Status. These docs describe the contract source of 2026-09-30. routr.near runs it on mainnet (since 2026-09-30); routr-dev.testnet runs development builds.

Concepts

Platform

A registered id (for example fastr) with an owner, a fee recipient, a creator_bps and a builder_bps. Registration is open to anyone and costs the record's storage plus a 0.5 NEAR bond that becomes the platform's storage credit. A platform can name up to 8 pool_creators (usually its factory) so nobody else opens pools under its id, and up to 32 approved builders it pays for routing orders.

Pool key

Every pool has a lookup key token|quote|platform_id that is known before the pool exists, so a token contract can embed its own route at creation. Methods that take a pool_id accept the numeric id or the key.

Curve

Price follows x · (y_real + y_virtual) = k. The seed amount and virtual_quote set the opening price (virtual_quote / seed). Sales pay out of the real quote reserve only, never the virtual one. If tokens exist outside the pool that were not bought from it, selling them can take the price below the opening price. A buy that would push the quote reserve past a fixed bound is refused, which bounds the arithmetic on that path.

Frozen terms

At create_pool the pool copies the protocol share, the protocol recipient, the platform recipient, creator_bps and builder_bps; the caller sets fee_bps. Later changes to the platform or the protocol apply to new pools only. The creator can hand its entitlement to another account, and the platform's list of approved builders applies to all its pools at once.

Quickstart

A launchpad's full path with near-api-js: register once, then per launch create the pool, seed it, quote and route a trade, and claim fees. One script runs on either network; NETWORK picks the routr account, wNEAR and the RPC. Before you run it:

  • routr needs a storage deposit on your token: call storage_deposit on the token for the routr account. Tokens that register their venues at creation, as fastr's do, need nothing. routr is already registered on mainnet wNEAR and USDC.
  • SEED is the amount of your token the script puts into the pool, in raw units; replace the example value with yours. A seed is final: there is no way to take it back out.
  • The example buy spends BUY wNEAR (default 1). Your account must hold that much wNEAR: call near_deposit on the wNEAR contract to wrap NEAR.

Mainnet

AccountWhat
routr.nearroutr
wrap.nearwNEAR, a quote token
17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1native USDC, a quote token
https://rpc.mainnet.fastnear.coman RPC endpoint (any mainnet RPC works)
npm i near-api-js tsx
NETWORK=mainnet PLATFORM=mypad ACCOUNT=mypad.near TOKEN=mytoken.mypad.near SEED=1000000000000000000000000000 npx tsx quickstart.ts

On mainnet the script spends real funds. Registering a platform attaches 0.52 NEAR, about $2.76 at $5.30 per NEAR on 2026-09-30: the 0.5 NEAR bond is kept as the platform's storage credit and is not refunded, and whatever the record's storage does not use of the rest comes back. Each pool attaches the create_pool_deposit that get_launch_status quotes; the unused part is refunded. Then it seeds SEED of your token for good and buys with BUY wNEAR, paying the pool fee (1% in the example) and any price impact. Platform ids are first come, first served and a platform cannot be deleted, so run the same script on testnet first. The signing key is read from ~/.near-credentials/mainnet/.

Testnet

AccountWhat
routr-dev.testnetroutr (development builds)
wrap.testnetwNEAR, a quote token
https://rpc.testnet.fastnear.coman RPC endpoint
NETWORK=testnet PLATFORM=mypad ACCOUNT=mypad.testnet TOKEN=mytoken.mypad.testnet SEED=1000000000000000000000000000 npx tsx quickstart.ts

Testnet NEAR is free from the NEAR faucet. routr-dev.testnet runs development builds, which can be ahead of mainnet and are reset when the stored layout changes. The signing key is read from ~/.near-credentials/testnet/.

The script

/**
 * routr quickstart for a launchpad (NEAR, near-api-js v5): register your platform once, then per launch
 * create the pool, seed it with the token supply, quote and route a first trade, and claim fees.
 *
 *   npm i near-api-js tsx
 *   NETWORK=mainnet PLATFORM=mypad ACCOUNT=mypad.near TOKEN=mytoken.mypad.near SEED=<raw amount> npx tsx quickstart.ts
 *   NETWORK=testnet PLATFORM=mypad ACCOUNT=mypad.testnet TOKEN=mytoken.mypad.testnet SEED=<raw amount> npx tsx quickstart.ts
 *
 * NETWORK picks routr's account, wNEAR and the RPC (mainnet: routr.near, wrap.near; testnet: routr-dev.testnet,
 * wrap.testnet). routr must hold a storage deposit on TOKEN before the seed (storage_deposit on the token for the
 * routr account); it already has one on wNEAR and native USDC on mainnet.
 *
 * On mainnet this spends real funds: the 0.5 NEAR platform bond (kept as storage credit, not refunded), the pool's
 * storage deposit, the SEED of your token (final: a seed cannot be withdrawn) and BUY wNEAR (default 1) for the
 * example buy, which ACCOUNT must already hold (near_deposit on wrap.near wraps NEAR). Try it on testnet first.
 *
 * Every call below is one contract method; the contract's own docs are in contracts/routr/src/lib.rs.
 * Amounts are strings of raw units (yocto for wNEAR). Pools are addressable by their key
 * `${token}|${quote}|${platform}` before they exist, so a token can carry its own route.
 */
import { connect, keyStores, Account, utils } from "near-api-js";

const NETWORK = process.env.NETWORK ?? "mainnet";
const NET = ({
  mainnet: { routr: "routr.near", wrap: "wrap.near", rpc: "https://rpc.mainnet.fastnear.com" },
  testnet: { routr: "routr-dev.testnet", wrap: "wrap.testnet", rpc: "https://rpc.testnet.fastnear.com" },
} as Record<string, { routr: string; wrap: string; rpc: string }>)[NETWORK];
if (!NET) throw new Error(`NETWORK must be mainnet or testnet, not ${NETWORK}`);
const ROUTR = process.env.ROUTR ?? NET.routr;
const PLATFORM = process.env.PLATFORM ?? "mypad";
const ACCOUNT = process.env.ACCOUNT!; // your platform's account (its factory/locker seeds pools)
const WRAP = NET.wrap;
const TGAS = (n: number) => BigInt(n) * 10n ** 12n;
const NEAR = (n: string) => utils.format.parseNearAmount(n)!;
const SEED = process.env.SEED; // raw units of TOKEN to seed; required, because a seed is final
const BUY = process.env.BUY ?? "1"; // wNEAR for the example buy

async function main() {
  const near = await connect({
    networkId: NETWORK,
    nodeUrl: NET.rpc,
    keyStore: new keyStores.UnencryptedFileSystemKeyStore(`${process.env.HOME}/.near-credentials`),
  });
  const me = await near.account(ACCOUNT);
  const view = (method: string, args: object) => me.viewFunction({ contractId: ROUTR, methodName: method, args });

  // 1. Register the platform once: 70% of the platform's part to each pool's creator, 5% to approved builders,
  //    only this account may create pools under the id. The 0.5 NEAR bond is the platform's storage credit for
  //    the claim keys its pools' swaps create (top it up any time with `platform_top_up`).
  const existing = await view("get_platform", { platform_id: PLATFORM });
  if (existing && existing.owner !== ACCOUNT) {
    throw new Error(`platform id ${PLATFORM} belongs to ${existing.owner}; pick another id`);
  }
  if (!existing) {
    await me.functionCall({
      contractId: ROUTR, methodName: "register_platform", gas: TGAS(30), attachedDeposit: BigInt(NEAR("0.52")),
      args: { platform_id: PLATFORM, fee_recipient: ACCOUNT, creator_bps: 7000, builder_bps: 500,
              pool_creators: [ACCOUNT], builders: [] },
    });
  }

  // 2. Per launch: check the key, create the pool, seed it. `virtual_quote` sets the starting price:
  //    opening price = virtual_quote / seed; seeding the whole supply with virtual_quote 1,000 NEAR opens the supply's
  //    value at 1,000 NEAR. This is an opening price, not a promise about any later price.
  const token = process.env.TOKEN ?? `mytoken.${ACCOUNT}`; // an NEP-141 whose supply this account holds
  if (!SEED || !/^[0-9]+$/.test(SEED)) throw new Error("set SEED to the raw amount of TOKEN to seed");
  const status = await view("get_launch_status", { token, quote: WRAP, platform_id: PLATFORM });
  console.log("launch status", status.state, "terms", status.terms, "deposit", status.create_pool_deposit);
  if (status.state === "absent") {
    await me.functionCall({
      contractId: ROUTR, methodName: "create_pool", gas: TGAS(30), attachedDeposit: BigInt(status.create_pool_deposit),
      args: { token, quote: WRAP, platform_id: PLATFORM, creator: ACCOUNT, seeder: ACCOUNT, fee_bps: 100,
              virtual_quote: NEAR("1000") },
    });
  }
  if (status.state !== "seeded") {
    // the seed is one ft_transfer_call of the supply; `expect_creator` binds the pool to the fee recipient you expect
    await me.functionCall({
      contractId: token, methodName: "ft_transfer_call", gas: TGAS(60), attachedDeposit: 1n,
      args: { receiver_id: ROUTR, amount: SEED,
              msg: JSON.stringify({ Seed: { pool_id: status.key, expect_creator: ACCOUNT } }) },
    });
  }

  // 3. Quote and route a buy: the quote is what you show before the user signs (fee waterfall, impact, executable).
  const amountIn = NEAR(BUY);
  const q = await view("quote_swap", { pool_id: status.key, token_in: WRAP, amount_in: amountIn, builder: null });
  if (!q) throw new Error(`no pool ${status.key}`);
  console.log("quote", q.amount_out, "impact bps", q.price_impact_bps, "executable", q.executable, q.reason ?? "");
  if (q.executable) {
    const minOut = (BigInt(q.amount_out) * 98n) / 100n; // 2% slippage
    await me.functionCall({
      contractId: WRAP, methodName: "ft_transfer_call", gas: TGAS(100), attachedDeposit: 1n,
      args: { receiver_id: ROUTR, amount: amountIn,
              msg: JSON.stringify({ Swap: { pool_id: status.key, min_out: minOut.toString(), recipient: null, builder: null } }) },
    });
  }

  // 4. Fees: booked per account at swap time; anyone can push them. A contract recipient reconciles with
  //    `get_claim_info` (cumulative creator leg vs cumulative delivered) instead of trusting a single push.
  const info = await view("get_claim_info", { pool_id: status.key, account: ACCOUNT });
  console.log("creator leg", info.fees_creator, "delivered so far", info.delivered);
  await me.functionCall({ contractId: ROUTR, methodName: "push", gas: TGAS(40), args: { account: ACCOUNT, token: WRAP } });

  // 5. Events for your indexer: NEP-297 standard "routr" (pool_created, pool_seeded, swap with every fee leg and
  //    the reserves after, payout). Page your own pools with get_platform_pools(platform_id, from, limit).
  console.log("my pools", await view("get_platform_pools", { platform_id: PLATFORM, from: 0, limit: 20 }));
}

main().catch((e) => { console.error(e); process.exit(1); });

The example registers a platform id only if it is free. If get_platform returns a record, check that its owner, recipient and pool creators are yours before using it.

Messages

Seeding and swapping are NEP-141 transfers to routr with a JSON msg. routr returns the unused amount, so a rejected seed or a swap below min_out costs only gas.

Seed (once, from the pool's seeder, the pool's token)

{"Seed": {"pool_id": "<id or token|quote|platform>", "expect_creator": "<account>"}}

expect_creator binds the seed to the creator the seeder expects; if the pool names someone else, the seed is refused.

Swap (either side of a seeded pool)

{"Swap": {"pool_id": "<id or token|quote|platform>", "min_out": "<raw units>", "recipient": null, "builder": null}}

recipient defaults to the sender. builder earns the platform's builder_bps only if the platform approved it; otherwise that leg stays with the platform.

Methods

MethodArgumentsCallerWhat it does
register_platformplatform_id, fee_recipient, creator_bps, builder_bps, pool_creators?, builders?anyone, payableRegisters a platform. Attach the record's storage plus the 0.5 NEAR bond (the rest is refunded). creator_bps + builder_bps ≤ 10,000. Up to 8 pool creators and 32 builders.
set_platformplatform_id, fee_recipient?, creator_bps?, builder_bps?, owner?, pool_creators?, builders?platform owner, payableChanges the platform. Fee terms apply to pools created afterwards; the creator and builder lists apply at once.
platform_top_upplatform_idanyone, payableAdds NEAR to a platform's storage credit.
create_pooltoken, quote, platform_id, creator, seeder?, fee_bps, virtual_quoteanyone (or the platform's pool creators), payableCreates an unseeded pool and freezes its terms. fee_bps in 1..1,000. Attach get_launch_status().create_pool_deposit; the unused part is refunded.
drop_poolpool_idplatform owner or seederRemoves an unseeded pool so its key can be created again. A seeded pool can never be removed.
set_pool_creatorpool_id, creatorthe pool's creator, payableHands the creator share to another account.
ft_on_transfersender_id, amount, msgNEP-141 tokenReceives a Seed or a Swap message (below). Returns the unused amount, which the token refunds.
pushaccount, tokenanyonePays what the venue holds for an account in a token (fee shares, undeliverable output).
withdrawtokenanyone, for themselvesThe caller's own push.
set_protocolprotocol_fee_bps?, protocol_recipient?protocol owner, payableThe protocol's share (≤ 3,000 bps) and where it goes, for pools created afterwards.
set_guardianguardian?protocol owner, payableNames or removes the guardian.
pauseguardian or ownerFreezes the venue at once: swaps and seeds return their input, every other user call fails with E_PAUSED, transfers already in flight still settle. Cancels a staged unpause.
stage_unpause / unpauseprotocol ownerAnnounces an unpause; it can land after upgrade_delay_ms.
stage_upgradecode_hash (sha256, base58)protocol ownerAnnounces new code by its hash; the upgrade_staged event carries the hash and the earliest time it can land (now + upgrade_delay_ms). Staging again restarts the clock.
cancel_upgradeprotocol ownerWithdraws a staged upgrade.
apply_upgradethe wasm bytes as the raw call input (not JSON)protocol owner, not payableAfter the delay, with bytes whose sha256 is the staged hash: schedules one receipt that re-checks the stage (check_dispatch), deploys the bytes and runs their migrate. A stage changed in between, or a failed migrate, fails that receipt and rolls the deploy back; the stage is consumed when migrate lands (upgrade_landed).
propose_owner / accept_ownerowner? / —protocol owner, payable / the proposed accountOwnership moves in two steps; accepting clears anything the previous owner had staged.

Views

ViewArgumentsReturns
get_launch_statustoken, quote, platform_idOne read for the pool key, state (absent, created, seeded), the terms, the estimated create_pool deposit, whether swaps are open and the platform's storage credit.
quote_swappool_id, token_in, amount_in, builder?A snapshot: output, the whole fee waterfall, price before and after, impact in bps, and whether the contract would currently accept it (reason when not: empty, reserve, storage, unseeded, not_in_pool). Null for an unknown pool. State can change before execution: always set min_out.
get_poolpool_idThe pool record: reserves, virtual quote, frozen terms, cumulative volume and fees. pool_id is the numeric id or the key.
get_pool_idtoken, quote, platform_idThe numeric id for a key, if the pool exists.
get_pricepool_idSpot price as [quote, token]; quote per token = price[0] / price[1].
get_poolsfrom, limitPools by id, at most 100 ids per call.
get_platformplatform_idThe platform record.
get_platform_poolsplatform_id, from, limitA platform's pools in creation order.
get_claim_infopool_id, accountThe pool's cumulative creator leg and what the venue has delivered to the account in the quote: the numbers a contract recipient reconciles.
get_owed / get_pending / get_deliveredaccount, tokenWhat the venue holds for an account, what is in flight, and what has arrived.
get_owed_totaltokenTotal liabilities in a token.
get_storageplatform_id?The venue's storage reserve and a platform's remaining credit, in swaps' worth of claim keys.
get_protocolOwner, pending owner, guardian, protocol share and recipient, both caps, pool count, paused, upgrade_delay_ms, the staged upgrade (hash, ready_ms) and a staged unpause's ready_ms.

Events

NEP-297 logs with standard: "routr", version: "1.0.0". Amounts are strings of raw units.

EventData
platform_registeredplatform_id, owner, fee_recipient, creator_bps, builder_bps, pool_creators, builders
platform_changedthe full platform record after the change
platform_topped_upplatform_id, by, amount, credit
pool_createdpool_id, token, quote, platform_id, creator, seeder, fee_bps, virtual_quote, terms
pool_seededpool_id and the seeded amount
pool_droppedpool_id, by
pool_creator_changedpool_id, creator
swappool_id, token, quote, trader, recipient, side, token_in, amount_in, token_out, amount_out, fee, fee_protocol, fee_platform, fee_creator, fee_builder, builder, platform_id, real_token, real_quote
swap_rejecteda swap refunded before it touched the pool, with the reason
payouttoken, account, amount, ok, why
protocol_changedprotocol_fee_bps, protocol_recipient, owner
guardian_changed / owner_proposed / owner_changedthe new guardian, pending owner or owner
paused / unpause_staged / unpausedby; ready_ms; —
upgrade_staged / upgrade_cancelled / upgrade_dispatched / upgrade_landedcode_hash (and ready_ms when staged). Watch upgrade_staged: it is the notice.
refused_pauseda seed or swap returned to its sender because the venue is paused
invariant_brokenshould never appear; emitted instead of failing a payout callback. Monitor it with the liability views

Errors

CodeMeaning
E_NO_PLATFORM / E_NO_POOLUnknown platform id or pool.
E_PLATFORM_EXISTS / E_POOL_EXISTSThe id or key is taken.
E_PLATFORM_IDPlatform ids are short lowercase identifiers.
E_BPSA share is above 10,000 or the two shares add up to more.
E_FEEPool fee outside 1..1,000 bps.
E_VIRTUAL_QUOTEVirtual quote is zero or above the bound.
E_NOT_A_POOL_CREATORThe platform restricts who may open its pools.
E_PLATFORM_OWNER / E_OWNER_ONLYOnly the platform owner or the protocol owner may call this.
E_NOT_THE_SEEDER / E_SEEDEDOnly the named seeder seeds, and only once.
E_CREATOR_MISMATCHThe seed's expect_creator differs from the pool's creator.
E_NOT_SEEDED / E_NOT_IN_POOLA swap into an unseeded pool, or with a token that is not one of the pool's two.
E_STORAGE_DEPOSITThe attached deposit does not cover the storage the call adds.
E_MSGThe transfer message is not a valid Seed or Swap.
E_PAUSEDThe venue is paused; only admin calls and in-flight settlements run.
E_NOT_GUARDIAN / E_NOT_PROPOSEDOnly the guardian or owner may pause; only the proposed owner may accept.
E_NOT_STAGED / E_NOT_YET / E_CODE_HASHNothing staged, the delay has not passed, or the bytes do not hash to the staged value.
E_STORAGE_HEADROOMThe account's balance would not cover the new code's storage plus the reserve for in-flight payouts; top it up first.
E_STALE_DISPATCHThe stage changed between apply_upgrade and its deploy receipt; nothing was deployed.

Fees and terms

A pool charges fee_bps (1 to 1,000) of the quote leg of each swap. From that fee the protocol takes protocol_fee_bps (2,000 today, capped at 3,000 in the code). The remainder is the platform's: creator_bps of it goes to the pool's creator, builder_bps of it to an approved builder named on the swap, and the rest to the platform's recipient. Every leg is visible in quote_swap before signing and in the swap event after.

Storage and payouts

Output and fee shares are liabilities first: a swap's output is recorded as pending while its transfer is in flight and becomes owed if the transfer fails (for example an unregistered recipient). push pays anything owed, for anyone, at any time. Claim records cost storage that traders do not attach, so each platform's bond is its own credit, charged per new claim record its pools create; a platform whose credit is spent has its pools' swaps refused until someone tops it up. The contract also checks the venue's shared storage room before accepting a swap. The payout callback is written not to panic: it uses saturating arithmetic and emits invariant_broken instead, so monitor get_pending, get_owed and that event.

Deployments

NetworkAccountBuild
testnetroutr-dev.testnetdevelopment build; reset when the stored layout changes
mainnetroutr.nearthe reproducible CI build below; owner routr.sputnik-dao.near (2 of 3), guardian guardian.routr.near, no access keys; not audited

Current reproducible CI build of the routr contract: code hash H4G6bhZXHKnhecs6gwgL3fQ4F3pt4RPHw34L2uaVhWdN (331,716 bytes, commit 76c5992). This is the build routr.near runs; see Security to check it.