@ton-primes/agent
v0.4.0
Published
TypeScript SDK for TON PRIMES: every action a program-player can take, each gated by a live get-method read, with the key signing locally and never leaving the process.
Maintainers
Readme
@ton-primes/agent
Play TON PRIMES from a program.
Integers are minted strictly sequentially as NFTs for a flat 1 TON. Composites need a
submitted prime factorization, primes are verified on chain by Miller–Rabin, and the
residual of every mint is injected into a ratchet whose internal floor price p_f = T / S
is strictly increasing on every mint. That last one is a contract-verifiable invariant, not
a promise — which is the point of this package.
Programs are welcome players (CONCEPT.md §9.10). Nothing here is a privilege: there is
no API key, no allow-list, and no contract path with a cooldown or a per-wallet cap. This
package is the published read endpoints, the same message builders the app signs with, and
a wallet that signs locally.
Complete by construction. Every message a program-player can send has a method, and
every route in the two published OpenAPI specs has a read. That is not a claim in this
file — it is test/coverage.test.ts, which reads all three lists out of the repository and
fails until a new builder or a new route is either wired or named as excluded with its
reason. See What is deliberately not here.
- Install
- Read first, spend second
- Play
- The rules this enforces for you
- API
- Errors
- What the game rewards a program for
Install
npm install @ton-primes/agentNode 20+, ESM, TypeScript declarations included. Peer-free: @ton/core, @ton/crypto and
@ton/ton are regular dependencies.
Read first, spend second
import { PrimesAgent } from "@ton-primes/agent";
const agent = new PrimesAgent({ network: "testnet" }); // read-only: no mnemonic
await agent.read.head(); // the next integer to be minted
await agent.read.floor(); // p_f as { numNanoton, denPrimes } — a ratio, never pre-divided
await agent.read.counters(); // T and S, and the two counters that are not deployed yet
await agent.read.number(7n); // who owns 7, and what tribute stands to it
await agent.routeFor(1234n); // "head" | "lot" | "past", read live off the chain
await agent.read.openLots(); // candidates from the index
await agent.read.openBuilds(); // builds still wanting a generator
await agent.read.lotPrice(5040n); // what opening a lot would cost, and which lane
await agent.read.discoveryOwed(addr); // unclaimed bounties, nanoPRIMES
await agent.read.contracts(); // which address holds which roleEvery read-proxy response carries a provenance object naming the get-method and the
contract address behind the figure, so anything the SDK returns can be recomputed by
calling that method yourself. Verify before you spend; the design assumes you will.
Three things the read layer will not do to you:
- No
Number()on a money figure. Everything monetary decodes tobigint. A nanoTON balance past 2⁵³ is ordinary, and rounding one is not an option. - No fabricated zero. A route whose get-method is not on the deployed contract
answers
501, and the matching method returnsnull. "The figure is nought" and "nothing published it" are different claims and stay different. - No index figure priced into a transaction.
read.*is for browsing. Everything underchain()is a liverunMethod, and that is what theactmethods check themselves against.
Play
const agent = new PrimesAgent({
mnemonic: process.env.PRIMES_MNEMONIC, // 24 words, a DEDICATED wallet
network: "testnet",
referralKey: 42, // a number you own: it is paid on every mint
});
await agent.mint(); // 1 TON for the number at the head
await agent.openLot({ n: 5040n }); // any number ahead of the head, prime or composite — opening IS the first bid
await agent.openHeadLot(9973n); // a prime's descending lot
await agent.bid({ n: 9973n }); // on a descending lot, the bid IS the purchase
await agent.closeLot(5040n); // permissionless, once an ascending clock has run
await agent.flush(); // permissionless: TON has no schedulerThe built number line (CONCEPT.md §3.2.2)
Four messages, in order, and the third is the one people miss: a build closes only while
the registrar holds its generator, and there is no send_to_build opcode — it is an ordinary
NFT transfer with the build named in its forward payload.
await agent.openBuild({ target: 23n, op: 18, inputs: [11n, 12n] });
await agent.proveInput({ target: 23n, inputIndex: 0 });
await agent.sendGenerator({ generatorAddress: "EQ...", target: 23n }); // the generator is BURNED on close
await agent.closeBuild(23n); // mints constellation 23
await agent.expireBuild(99n); // permissionless, pays nothing, frees a targetGetting paid
await agent.claimTribute(7n); // §4.1's divisor share, off 7's own item
await agent.claimDiscovery(); // D-107's bounties, off the bounty vault
await agent.annotate({ n: 30n, text: "the third primorial" }); // §4.7.1, an era trophy's free first engraving
await agent.transferTrophy({ primorial: 30n, newHolder: "EQ..." }); // D-14, holder onlyThe factorization and each ⌊√p⌋ are computed for you from the same modules the contract
tests use, so a mint cannot carry a proof the ledger will reject.
The rules this enforces for you
- A number you are about to spend against is re-read from the chain.
mintchecks the live head,bidchecks the livegetLot(n). The HTTP index finds candidates; it never prices a transaction. - Every refusal the contract would make is made first, off chain. Not the owner, a
zero
owed, an empty bounty line, an input index pastinputCount, a build holding no generator, an expiry head the head has not reached, a naming right already spent, an emptypending, a flush inside its 24-hour cooldown. Each is a thrownErrorbefore a wallet opens, where the contract's version of the same refusal is gas already paid. - Nominal price plus a fee margin, never the exact price. The contract compares the value that arrives, after TON deducts the message's forward fee. Sending the exact nominal reverts.
- Throttled at the client. read-proxy allows 600 requests a minute per IP, read-index
120; both answer
429withRetry-After. The default is 2 requests a second with one bounded retry, and it fails loudly rather than queueing on your behalf. - Your key never leaves your process. No service in this project holds one.
API
new PrimesAgent(options)
| Option | Default | |
| --- | --- | --- |
| mnemonic | — | 24 words. Omit for a read-only agent: every action then refuses by name. |
| network | "testnet" | Selects the default Toncenter endpoint. |
| tonEndpoint, tonApiKey | Toncenter public | Your own RPC, if you have one. |
| addresses | from /contracts | Skip the manifest fetch by passing the roles yourself. |
| referralKey | 0 | §4.1's referral line: a number you own. 0 routes it to the pool, never to the team. |
| readProxyUrl, readIndexUrl, eventsUrl | primes.live | The three services. |
| rps | 2 | Client-side pacing, across all three. |
| fetchImpl | globalThis.fetch | Injectable, for tests and proxies. |
Actions — every message a program-player can send
| Method | Sends to | Costs |
| --- | --- | --- |
| mint({ n?, beneficiary?, referralKey?, invites? }) | ledger | 1 TON |
| bid({ n, bidNanoton?, beneficiary?, referralKey? }) | market | the bid |
| openLot({ n, bidNanoton?, … }) | market | the opening bid |
| openHeadLot(n) | market | gas |
| closeLot(n) | market | gas |
| openBuild({ target, op, inputs, inputIsCon? }) | registrar | gas |
| proveInput({ target, inputIndex }) | registrar | gas |
| sendGenerator({ generatorAddress, target }) | the generator item | gas — burns the generator |
| closeBuild(target) | registrar | gas + the primality check |
| expireBuild(target) | registrar | gas, pays nothing |
| claimTribute(n) | n's own item | gas |
| topUp(n, multiples?) | n's own item | multiples x the item endowment — permissionless on ANY number |
| claimDiscovery() | bounty vault | gas |
| annotate({ n, text }) | registrar | gas — once only, era trophies only (D-179) |
| transferTrophy({ primorial, newHolder }) | registrar | gas — holder only (D-14) |
| flush() | ratchet | gas |
| swap({ side, amount, slippageBps? }) | DeDust's native vault (buy) or this wallet's own PRIMES jetton wallet (sell) | the amount + the venue's gas |
Reads
agent.read is a PrimesRead over the three published services, and
agent.chain() is a PrimesChain of live runMethod calls. Grouped:
- the ratchet and the ledger —
head,floor,counters,pending,swapped,flushStatus,floorCache,staking,k,kMax,lastMintTime,seedT,eraLeader,bootstrap - one wallet —
balance,seqno,primesBalance,ranks,earn,numbersOf,claimsOf,historyOf,contestOf,portfolioOf - numbers, lots and prices —
number,numbers,isKnownPrime,itemAddress,name,numberTrophy,runway,tributeTotal,yieldOf,lot,lotPrice,lotFloor,score,trophy,trophyParams,isOffHead,feeSplit,openLots,lotHistory,cheapestLot,unclaimedOffers - invites, generators and the built line —
inviteConfig,generator,generatorsOf,generatorRefusal,build,openBuilds,constellation,buildTotals,constellationsOf,recentConstellations,collections - the bounty vault —
vault,discoveryOwed,discoveryTier,eraSchedule,rebate - the auditor's surfaces —
supply,solvency,sink,venue,market,vesting,treasury,treasuryProposals,treasuryLedger,stake,stakePositions,stakeLedger,lpPositions,lpLocked,lpWallet,contracts - the index's own tables —
indexStatus,board,feed
Anything not named: read.proxy<T>(path), read.proxyOptional<T>(path),
read.index<T>(path) and read.events<T>(path) are the escape hatches, one per service.
Helpers
factorize(n), isqrt(v), isProbablePrime(n) — the contract's own arithmetic, so a
proof you build with them is a proof the ledger accepts. NAME_MAX_CHARS,
BID_FEE_MARGIN.
Errors
Everything throws a plain Error with a message written to be read by whoever has to fix
it. Four kinds, and they are worth telling apart:
| Looks like | Means |
| --- | --- |
| this agent is read-only: … | constructed without a mnemonic. Thrown before any network call. |
| n=99 is not the head (12); … | a pre-check refused what the contract would have refused. Nothing was sent, nothing was spent. |
| GET …/ledger/head -> 429 twice; slow down | the rate-limit budget is genuinely spent. Lower rps; the SDK will not queue for you. |
| wallet seqno did not advance within 45000ms | the transfer was signed and sent, and confirmation timed out. The message may still land — re-read state before retrying. |
null from a read is never an error: it is a route whose get-method is not on the deployed
contract, and the methods that can return it say so in their own doc comments.
What the game rewards a program for
- The constellation discovery bounties are finite, first-come windows over a
deterministic graph (
CONCEPT.md§3.2.2). They go to whoever enumerates it first. - A referral key is a bearer asset.
rNpays the current owner of numberNon every mint made under it, including your own. flush()is permissionless. TON has no scheduler, so the ratchet relies on someone calling it.
What it does not reward: a loop. Every automated behaviour is priced at the floor in
CONCEPT.md §8 — a mint loop returns less than it pays, self-referral is assumed universal
rather than prevented, and a recruit costs a fresh address plus a full 1 TON mint. §8.1
names the one regime where that inverts and publishes the threshold. Read it before you
model a return.
The reference agent
examples/prospector.ts enumerates every constellation target the numbers in a wallet can
reach, rarest operation first, and filters out the ones someone is already building.
It lives in the repository rather than the tarball, because it imports the SDK's sources:
git clone https://github.com/KonnikPahoni/primes && cd primes/backend/agent
PRIMES_WALLET=EQ... npx tsx examples/prospector.tsWhat is deliberately not here
The LP miner and the governed treasury (DECISIONS.md D-100 — eight message builders).
A deposit has to leave from the depositor's DeDust LP jetton wallet, and read-proxy reports
that address as null on every deployment because the DeDust master is not a configured
role. Every position message needs a position, and a position needs a deposit. They are not
wrapped as though they worked.
The paid annotation (§5.2 family 1) is absent for a smaller reason: it is a TEP-74
transfer out of your own PRIMES jetton wallet, priced at 1 TON of PRIMES at the live floor
(getNamePriceTon() against the ledger's T/S, served as read.sink().namePricePrimes,
D-179). Since D-179 that is how every prime is named, so an agent can name only an era
trophy (free, once, through annotate()).
Ten read routes, each for a stated reason: a liveness probe, an SSE stream, rendered
artwork, TEP-64 metadata served for a wallet's own fetcher, and four narrative surfaces the
webapp computes for its own charts. test/coverage.test.ts carries the list with the reason
per line, and a new route in either spec fails that test until somebody classifies it.
Reference
- The action loop and the OpenAPI specs: https://primes.live/llms.txt
- The spec: https://primes.live/docs/CONCEPT.md (§9.10 is the stance on programs)
- The audit pack: https://primes.live/docs/audit/
- The MCP server over this SDK:
@ton-primes/mcp - Changelog: CHANGELOG.md
