An unofficial command-line interface for the Pons launchpad on Robinhood Chain (chain ID 4663), built for developers and agents alike. Not affiliated with Pons or Robinhood; it reads public contracts on a public chain.
- Read-only by default. The CLI runs without ever seeing a private key. Signing is an explicit opt-in, so an agent cannot spend by accident.
- Structured output everywhere.
--jsonon every command, enabled automatically when stdout is not a terminal. Errors are structured too, with stable exit codes. - Protocol values are read from the chain. Fees, tax windows and the approved pair-token list are queried live, never hard-coded. The on-chain values differ from the contract source.
- Two-tier RPC. Free endpoints round-robined in Tier 1; a paid endpoint in Tier 2, reached only after Tier 1 is fully exhausted. Without a paid credential, bulk work degrades rather than turning an outage into an invoice.
- Two binaries.
ponsfor people,pons-mcpfor models, over the same core, so the price an agent is quoted is the price the CLI prints.
npm install -g ponscli
pons --versionOr without installing anything. The package is ponscli and the binary is
pons, so npx has to be told which one to run:
npx -p ponscli pons info 0x44D6…20f4Two Node versions matter, and they are not the same one. The CLI runs on
20.11 or newer, which CI checks on every push by installing the packaged
tarball on exactly that version. Building it needs 20.19 or newer, because
the linter's dependency tree requires it. From a clone, npm install builds as
part of the install, and npm run dev -- <command> runs it from source.
Reading needs no key, no wallet and no configuration.
pons info 0x44D6…20f4 # price, reserves, graduation phase, live snipe tax
pons pairs # every quote asset a launch may be priced in
pons watch 0x44D6…20f4 # follow one launch's trades as they happen
pons tx 0x2e8d…f58e # receipt, decoded logs, and why a failure failed
pons doctor # probe every RPC endpoint with real callsTrading needs a key. Nothing is signed unless you say so.
pons wallet create # generate a key into an encrypted keystore
pons buy 0x44D6…20f4 0.05 # price it, simulate it, print it. Nothing is sent
pons buy 0x44D6…20f4 0.05 --dry-run # the same, said explicitly
pons buy 0x44D6…20f4 0.05 --confirm # sign and broadcast
pons sell 0x44D6…20f4 50% --slippage 200Everything is machine-readable when piped, and that is the default:
pons info 0x44D6…20f4 | jq .graduationFull reference, including which contract each command touches: docs/commands.md.
Reading, no key required
pons info <token> |
Price, supply, graduation progress, fees, live snipe tax |
pons pairs |
Approved quote assets, in the order the Pons client renders them |
pons watch <token> |
Curve trades, buybacks and graduation, followed with a cursor |
pons tx <hash> |
Receipt, decoded logs, and the revert reason behind a failure |
Trading, with RPC and a key
pons buy <token> <amount> |
Buy on whichever venue the token trades |
pons sell <token> <amount|50%|all> |
Sell on the same |
pons graduate <token> |
Finish a curve that has raised its threshold. Anyone may call this |
Launching
pons launch --name X --symbol Y |
Create a token. V2 by default |
Fees
pons claim |
V2 creator or protocol fees held in the escrow |
pons collect <token> |
V1 position fees held by the locker |
pons vault show|release <token> |
The V2 buyback vault |
Wallet and setup
pons wallet create|import|show|export |
Key management |
pons wallet balance|track|untrack |
Holdings |
pons wallet transfer|sweep |
Spend |
pons config list|get|set|unset|path |
Configuration, and where each value came from |
pons doctor |
Endpoint health, probed with the calls the CLI actually makes |
Pons has launched tokens through two factories, and both are supported. You never have to say which one you mean: both registries are asked, and the answer settles everything after it.
| V1 | V2 | |
|---|---|---|
| Venue | a Uniswap V3 pool, from the first block | a bonding curve, graduating to Uniswap V4 |
| Quote asset | WETH | ETH, or one of 23 approved tokens |
| Opening protection | wallet and transaction caps, for two blocks | a snipe tax, 99% decaying over three seconds |
| Liquidity | locked forever; only its fees come out | seeded into the pool at graduation |
| Fees | pons collect |
pons claim |
| New launches | closed | open |
V1's factory is retired. launchEnabled() has been false since 2026-08-12, and the transaction that set it is the last thing the factory ever emitted; no address has ever been whitelisted past it, so pons launch --generation v1 refuses before building anything. Its tokens are another matter. Thousands still hold liquidity, and a forty-pool sample saw 223 swaps in under six days, which is why reading and trading them is fully supported.
Every write builds a Plan first: what it will send, to whom, what it expects back, and what is worth warning about. That same Plan is what gets simulated and what gets signed, so the three cannot drift apart.
| mode | what happens |
|---|---|
| (no flag) | Build the plan, simulate it against live state with a balance override, and print both. Nothing is signed or sent |
--dry-run |
The same work, asked for explicitly: "mode": "dry-run" in the payload, and no reminder that nothing was sent |
--unsigned |
Emit calldata and value for somebody else to sign. The only mode that does not simulate |
--confirm |
Sign and broadcast |
Four behaviours worth knowing:
- The venue is not a choice you have to make. The launch record says where the token trades.
--route curve|v4|v3overrides that answer rather than searching for one. - The floor is fixed when you accept it. The plan is rebuilt from live state immediately before broadcasting, and the floor you accepted is carried into the rebuild rather than recomputed from the new price. A trade cannot be signed below the bound you approved, and if the price has moved past it nothing is sent at all.
- A sell into a curve at its threshold is redirected, not failed. The curve stops trading once it has raised enough, and
pons graduate <token>is a call anyone can make. - A V1 sell is unwrapped for you. V1 pools hold WETH rather than ETH, so a sell leaves its proceeds at the router and unwraps them in the same transaction. Selling the obvious way would work and hand you WETH.
pons launch --name "My Token" --symbol MINE --desc "..." # plan it and simulate it. Nothing is sent
pons launch --name "My Token" --symbol MINE --dry-run # the same, said explicitly
pons launch --name "My Token" --symbol MINE \
--pair USDG --creator-tax 250 --dev-buy 100 --confirm # launch, quoted in USDGA launch is the one thing here that cannot be undone. Before it builds a transaction the command prints the whole of what you are committing to: the address the token will land at, the supply, where it graduates, the fee split, and what the snipe tax will do. It also checks locally everything the factory would reject on chain.
- The address is known before you send. Both generations deploy with CREATE2 from a salt you choose, so
pons launchprints the token and curve addresses in advance and refuses a salt that is already taken.--saltaccepts a raw 32-byte value, so a mined vanity address works. - The terms are pinned.
previewLaunchEconomicsreturns a digest covering the supply, the curve fee, the pool's fee tier and the protocol's fee split, and it travels with the launch. If the protocol owner changes any of them while your transaction is in flight, the launch reverts rather than quietly repricing. --dev-buyis atomic, and it has to be. The V2 factory demands exactly the launch fee and nothing more, so an opening buy cannot ride along with it; the buy goes through the launchpad's own router in the same transaction instead. A launch without one has been bought out inside two blocks on this chain.--buybackcosts you, not holders. The vault is funded entirely from your own fee share and releases over five years, split with the protocol. It is not a holder distribution, and the plan says so as a warning rather than a footnote.
pons pairs lists the assets a launch may be priced in: native ETH plus the twenty-three approved tokens, which are tokenised equities, two index funds and one stablecoin. They come back in the order the Pons create page renders them, so the two agree row for row.
- Read-only is the default. No command signs anything without
--confirm. Three of the four execution modes cost nothing, and every one of them but--unsignedproves the call against the deployed contracts first, without the account holding funds. - The keystore is scrypt + AES-256-GCM, written owner-only. A password comes from a hidden prompt or
PONS_PASSWORD; a key to import comes fromPONS_PRIVATE_KEY. - Neither is ever a flag. An argument is visible in the process table and lands in shell history. There is no flag that takes either, and
pons config listsays so where somebody would look for one. - Credentials never reach a log line. A paid endpoint carries its key in the URL and error messages embed URLs, so URLs are masked inside thrown errors, not just in formatted output.
- The MCP server cannot sign. It never loads a keystore and never broadcasts. See
docs/agents.md. - A native transfer is signed with a fixed 100,000 gas limit. Nitro charges the L1 posting cost out of the transaction's own limit and
eth_estimateGasdoes not say so; at 21,000 a transfer works while L1 is cheap and fails intermittently when it is not.
Found something that could cost somebody funds? Please report it privately. SECURITY.md has the how, what is in scope, and what the design guarantees.
Values resolve through a ladder: flag > environment > config file > default. pons config list prints the resolved value alongside the tier it came from, so a surprising setting is one command away from being explained.
The config file is flat JSON at $XDG_CONFIG_HOME/ponscli/config.json (falling back to ~/.config/ponscli/config.json), written owner-only.
| Key | Environment | Default |
|---|---|---|
rpc.url |
PONS_RPC_URL |
unset. Your own node, tried first |
rpc.endpoints |
PONS_RPC_ENDPOINTS |
the official and nodeflare public endpoints |
rpc.alchemyKey |
PONS_ALCHEMY_KEY |
unset, which disables the paid tier |
rpc.tier |
PONS_RPC_TIER |
auto |
rpc.timeoutMs |
PONS_RPC_TIMEOUT_MS |
10000 |
wallet.keystore |
PONS_KEYSTORE |
<config dir>/keystore.json |
wallet.tracked |
PONS_WALLET_TRACKED |
empty |
output.json |
PONS_JSON |
on when stdout is not a TTY |
output.color |
PONS_COLOR |
auto (honours NO_COLOR) |
trade.slippageBps |
PONS_SLIPPAGE_BPS |
100 (1%) |
trade.priorityFeeGwei |
PONS_PRIORITY_FEE_GWEI |
unset |
cache.dir |
PONS_CACHE_DIR |
<cache dir>/ponscli |
PONS_PASSWORD and PONS_PRIVATE_KEY are read from the environment but are deliberately not config keys: neither belongs in a file that config list prints and that users paste into issue reports.
The full document, holding every decision and the measurement behind it, is docs/architecture/ponscli.md. The shape in brief:
src/core/ business logic, framework-free
src/commands/ thin CLI wiring
src/mcp/ the MCP server, also thin
src/chain/ the RPC waterfall
src/abi/ generated and committed
Both front ends are shells over src/core/. That is what makes pons-mcp a wrapper rather than a second implementation.
Tier 1 is a round-robin over free endpoints; Tier 2 is a paid endpoint reached only once every Tier 1 candidate has been tried. Every rule comes from a measurement against this chain's own endpoints rather than from a general-purpose retry policy. The ones that matter most day to day:
- A revert is an answer, not a failure. It comes straight back to you instead of being retried across every endpoint and then escalated to the paid tier.
- A rate limit parks an endpoint for 30 seconds, not an hour. Free endpoints are token buckets; a long park costs more availability than the throttling did.
- Writes never round-robin. A nonce taken from one node and broadcast to another is
nonce too low, so transactions pin a single endpoint. eth_chainIdis not a health check. One public endpoint answers it faster than any other and then rejectseth_call.pons doctorprobes with real calls for exactly this reason.
src/abi/ is generated and committed, so the CLI works offline and an interface change shows up as a reviewable diff.
npm run abi:fetch # regenerate from Sourcify, falling back to Blockscout
npm run abi:check # fail if the committed files have driftedEach file records where its ABI came from and how the match was proven. There are four provenance classes, because the contracts differ:
| class | example | how it is proven |
|---|---|---|
| Verified at its address | PonsV2LaunchFactory |
Sourcify's own bytecode match |
| Deployed identically on every chain | Permit2 | verified on Base; every selector present in Robinhood's bytecode |
| Deployed once per launch, verified nowhere | the V2 curve and token | compiled from the factory's verified standard-JSON input, matched byte for byte against a live instance outside its immutables |
| Deployed many times, verified at some | the V3 pool, V1's token | one verified instance, proven general against a second, independently chosen one |
One file is written by hand: erc20Errors.ts, holding the errors third-party quote assets revert with. A quote asset is somebody else's token, there is no single ABI covering the twenty-three approved ones, and without these the most ordinary failure on a USDG launch prints as four unexplained bytes.
Full guide: docs/agents.md.
pons-mcp ships in this package as a second binary and calls into the same core/ the CLI does.
Six tools: pons_info, pons_pairs, pons_plan_buy, pons_plan_sell, pons_transaction, pons_endpoints.
The server cannot sign, and there is no flag that makes it. It reads the chain and it builds unsigned transactions; a person sends them. That is the whole security model, and it is why the server is safe to leave running next to a model: the worst a confused agent can do is quote you a bad price, not spend your money.
Answers come back in base units, because a model can do exact arithmetic on 50000000000000000 and cannot on 0.05.
For an agent shelling out to the CLI instead, --json is already the default when stdout is not a terminal, and an argument mistake carries the failing command's usage line so the caller can correct itself:
{
"ok": false,
"error": {
"code": "USAGE",
"message": "missing required argument 'token'",
"hint": "run 'pons info --help'",
"details": { "command": "info", "usage": "pons info [options] <token>" }
}
}This repository carries its own agent configuration. CLAUDE.md is the project brief, with AGENTS.md symlinked to it for Codex, Cursor, Copilot and others.
.claude/hooks/block-secrets.sh |
Refuses any command line holding a private key or an inline password |
.claude/hooks/warn-broadcast.sh |
Says out loud when --confirm or pons launch is about to spend real money |
.claude/skills/verify/ |
Runs the checks this project gates on, and explains what each failure means here |
npm run typecheck
npm run lint
npm test
npm run buildThe suite is entirely offline. Every test drives the real command surface against test/fakeChain.ts, a table of address + ABI + function → result that serves Multicall3 and eth_getLogs too, so what is exercised is the same decoding and aggregation the live path uses.
Two checks reach the network and are deliberately not part of npm test:
npm run test:sequence # prove multi-block sequences against live mainnet state
npm run abi:check # fail if a committed ABI has drifted from its verified sourceA simulation proves one call against one block. It cannot prove that an approval lands and the sell in the next block then succeeds, or that a launch confirms and the curve it created is buyable a block later. Those are the failures that reach a user.
Robinhood Chain answers eth_simulateV1, which runs several blocks of several calls with state carrying across all of them, against live mainnet state. npm run test:sequence uses it to prove six sequences, including the whole life of a launch: created, traded, taken past its graduation threshold, and seeded into a Uniswap V4 pool. No local node, no Foundry, nothing spent.
Every sequence carries a control that must fail. Without one, a state override could be doing the work that the sequence appears to prove.
scripts/mainnet-e2e.sh launches a token, buys it, sells it and claims the fees against mainnet, for real. Sequences are covered for free by the check above, so what this adds is the one thing simulation cannot: a real transaction, signed by a real key, accepted by the real mempool, with a real receipt to decode. It is off unless you say so, asks before every transaction, and must never run in CI.
PONS_E2E=i-understand ./scripts/mainnet-e2e.shScripts and agents can branch on the outcome without parsing prose.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Unclassified failure |
2 |
Usage: bad flags, arguments or command |
3 |
Configuration is missing, malformed or contradictory |
4 |
Every RPC endpoint failed |
5 |
The chain answered, and the answer was a revert |
6 |
Keystore missing, wrong password, or signing refused |
7 |
The user declined a confirmation |
8 |
The account cannot pay for what was asked |
In --json mode a failure writes {"ok": false, "error": {...}} to stderr, never stdout, so a pipe carries only results.
See CONTRIBUTING.md, and SECURITY.md for anything that should not be a public issue. The short version: run the four checks above, keep new behaviour in src/core/, and verify any claim about the protocol against mainnet before writing it down. The contract sources are stale in several places that matter.
Written from scratch in TypeScript on viem. No source is copied from any repository: the command surface is derived from a behavioural map of chainstacklabs/pumpfun-cli, and the RPC policy from measurements taken against this chain's own endpoints.
Pons itself is at ponsfamily.com: the web client for launching and exploring, its analytics, and its documentation. This CLI is not part of it, is not endorsed by it, and talks to the same public contracts anybody else can.
MIT. See LICENSE.