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-dev.testnet runs a development build of it and is reset when the stored layout changes. Mainnet at routr.near follows an external review.

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. Run it against testnet with ROUTR=routr-dev.testnet.

/**
 * 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
 *   ROUTR=routr-dev.testnet PLATFORM=mypad ACCOUNT=mypad.testnet npx tsx quickstart.ts
 *
 * 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 ROUTR = process.env.ROUTR ?? "routr-dev.testnet";
const PLATFORM = process.env.PLATFORM ?? "mypad";
const ACCOUNT = process.env.ACCOUNT!; // your platform's account (its factory/locker seeds pools)
const WRAP = "wrap.testnet";
const TGAS = (n: number) => BigInt(n) * 10n ** 12n;
const NEAR = (n: string) => utils.format.parseNearAmount(n)!;

async function main() {
  const near = await connect({
    networkId: "testnet",
    nodeUrl: "https://rpc.testnet.fastnear.com",
    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
  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: "1000000000000000000000000000",
              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("1");
  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?, owner?protocol owner, payableThe only owner method: the protocol's share (≤ 3,000 bps) and where it goes, for pools created afterwards.

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, protocol share and recipient, both caps, and the pool count.

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
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.

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.nearafter the external review, from the reproducible CI build

Current reproducible CI build of the routr contract: code hash 8BKXgNYV8Qzr3HJZuNqS2Vee1zP28nax9rFDhW3iSuNC (301,886 bytes, commit f277aba). The reviewed build deployed to mainnet may differ and its hash will be published here; see Security.