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
| Method | Arguments | Caller | What it does |
|---|---|---|---|
register_platform | platform_id, fee_recipient, creator_bps, builder_bps, pool_creators?, builders? | anyone, payable | Registers 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_platform | platform_id, fee_recipient?, creator_bps?, builder_bps?, owner?, pool_creators?, builders? | platform owner, payable | Changes the platform. Fee terms apply to pools created afterwards; the creator and builder lists apply at once. |
platform_top_up | platform_id | anyone, payable | Adds NEAR to a platform's storage credit. |
create_pool | token, quote, platform_id, creator, seeder?, fee_bps, virtual_quote | anyone (or the platform's pool creators), payable | Creates 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_pool | pool_id | platform owner or seeder | Removes an unseeded pool so its key can be created again. A seeded pool can never be removed. |
set_pool_creator | pool_id, creator | the pool's creator, payable | Hands the creator share to another account. |
ft_on_transfer | sender_id, amount, msg | NEP-141 token | Receives a Seed or a Swap message (below). Returns the unused amount, which the token refunds. |
push | account, token | anyone | Pays what the venue holds for an account in a token (fee shares, undeliverable output). |
withdraw | token | anyone, for themselves | The caller's own push. |
set_protocol | protocol_fee_bps?, protocol_recipient?, owner? | protocol owner, payable | The only owner method: the protocol's share (≤ 3,000 bps) and where it goes, for pools created afterwards. |
Views
| View | Arguments | Returns |
|---|---|---|
get_launch_status | token, quote, platform_id | One 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_swap | pool_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_pool | pool_id | The pool record: reserves, virtual quote, frozen terms, cumulative volume and fees. pool_id is the numeric id or the key. |
get_pool_id | token, quote, platform_id | The numeric id for a key, if the pool exists. |
get_price | pool_id | Spot price as [quote, token]; quote per token = price[0] / price[1]. |
get_pools | from, limit | Pools by id, at most 100 ids per call. |
get_platform | platform_id | The platform record. |
get_platform_pools | platform_id, from, limit | A platform's pools in creation order. |
get_claim_info | pool_id, account | The 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_delivered | account, token | What the venue holds for an account, what is in flight, and what has arrived. |
get_owed_total | token | Total liabilities in a token. |
get_storage | platform_id? | The venue's storage reserve and a platform's remaining credit, in swaps' worth of claim keys. |
get_protocol | Owner, 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.
| Event | Data |
|---|---|
platform_registered | platform_id, owner, fee_recipient, creator_bps, builder_bps, pool_creators, builders |
platform_changed | the full platform record after the change |
platform_topped_up | platform_id, by, amount, credit |
pool_created | pool_id, token, quote, platform_id, creator, seeder, fee_bps, virtual_quote, terms |
pool_seeded | pool_id and the seeded amount |
pool_dropped | pool_id, by |
pool_creator_changed | pool_id, creator |
swap | pool_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_rejected | a swap refunded before it touched the pool, with the reason |
payout | token, account, amount, ok, why |
protocol_changed | protocol_fee_bps, protocol_recipient, owner |
invariant_broken | should never appear; emitted instead of failing a payout callback. Monitor it with the liability views |
Errors
| Code | Meaning |
|---|---|
E_NO_PLATFORM / E_NO_POOL | Unknown platform id or pool. |
E_PLATFORM_EXISTS / E_POOL_EXISTS | The id or key is taken. |
E_PLATFORM_ID | Platform ids are short lowercase identifiers. |
E_BPS | A share is above 10,000 or the two shares add up to more. |
E_FEE | Pool fee outside 1..1,000 bps. |
E_VIRTUAL_QUOTE | Virtual quote is zero or above the bound. |
E_NOT_A_POOL_CREATOR | The platform restricts who may open its pools. |
E_PLATFORM_OWNER / E_OWNER_ONLY | Only the platform owner or the protocol owner may call this. |
E_NOT_THE_SEEDER / E_SEEDED | Only the named seeder seeds, and only once. |
E_CREATOR_MISMATCH | The seed's expect_creator differs from the pool's creator. |
E_NOT_SEEDED / E_NOT_IN_POOL | A swap into an unseeded pool, or with a token that is not one of the pool's two. |
E_STORAGE_DEPOSIT | The attached deposit does not cover the storage the call adds. |
E_MSG | The 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
| Network | Account | Build |
|---|---|---|
testnet | routr-dev.testnet | development build; reset when the stored layout changes |
mainnet | routr.near | after 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.