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_depositon 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. SEEDis 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
BUYwNEAR (default 1). Your account must hold that much wNEAR: callnear_depositon the wNEAR contract to wrap NEAR.
Mainnet
| Account | What |
|---|---|
routr.near | routr |
wrap.near | wNEAR, a quote token |
17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1 | native USDC, a quote token |
https://rpc.mainnet.fastnear.com | an 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
| Account | What |
|---|---|
routr-dev.testnet | routr (development builds) |
wrap.testnet | wNEAR, a quote token |
https://rpc.testnet.fastnear.com | an 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
| 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? | protocol owner, payable | The protocol's share (≤ 3,000 bps) and where it goes, for pools created afterwards. |
set_guardian | guardian? | protocol owner, payable | Names or removes the guardian. |
pause | guardian or owner | Freezes 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 / unpause | protocol owner | Announces an unpause; it can land after upgrade_delay_ms. | |
stage_upgrade | code_hash (sha256, base58) | protocol owner | Announces 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_upgrade | protocol owner | Withdraws a staged upgrade. | |
apply_upgrade | the wasm bytes as the raw call input (not JSON) | protocol owner, not payable | After 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_owner | owner? / — | protocol owner, payable / the proposed account | Ownership moves in two steps; accepting clears anything the previous owner had staged. |
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, 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.
| 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 |
guardian_changed / owner_proposed / owner_changed | the new guardian, pending owner or owner |
paused / unpause_staged / unpaused | by; ready_ms; — |
upgrade_staged / upgrade_cancelled / upgrade_dispatched / upgrade_landed | code_hash (and ready_ms when staged). Watch upgrade_staged: it is the notice. |
refused_paused | a seed or swap returned to its sender because the venue is paused |
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. |
E_PAUSED | The venue is paused; only admin calls and in-flight settlements run. |
E_NOT_GUARDIAN / E_NOT_PROPOSED | Only the guardian or owner may pause; only the proposed owner may accept. |
E_NOT_STAGED / E_NOT_YET / E_CODE_HASH | Nothing staged, the delay has not passed, or the bytes do not hash to the staged value. |
E_STORAGE_HEADROOM | The account's balance would not cover the new code's storage plus the reserve for in-flight payouts; top it up first. |
E_STALE_DISPATCH | The 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
| Network | Account | Build |
|---|---|---|
testnet | routr-dev.testnet | development build; reset when the stored layout changes |
mainnet | routr.near | the 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.