npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fyuz-sdk

v0.1.1

Published

TypeScript SDK for the Fyuz public REST API — bonding-curve token market data on BNB Smart Chain.

Readme

🚀 fyuz-sdk

Bonding-curve market data and unsigned trades on BNB Smart Chain — in one zero-dependency TypeScript package.

version npm language runtime deps node module license

Homepage · API · Repository · Go · Python · Rust


Fyuz is a bonding-curve token launchpad on BNB Smart Chain (chain 56). A token trades on an internal curve until it reaches a $30,000 market cap, at which point it graduates into a PancakeSwap V2 pair. This package is a typed client for the public Fyuz REST API — discovery feeds, token detail, trades, OHLCV candles, analytics, portfolios, leaderboards and the Distributor ledger — plus a trade builder that produces unsigned { to, data, value } transactions your own wallet signs. Every endpoint is unauthenticated and safe to poll. Nothing here has a runtime dependency, and nothing here has ever seen a private key.


📦 Install

pnpm add fyuz-sdk

⚠️ Not on the registry yet. fyuz-sdk is not published, so the command above 404s today. Until it lands, build from the repository: clone it, then pnpm install && pnpm build in typescript/. The other three SDKs are live — go get github.com/fyuz-launchpad/fyuz-sdk/go, pip install fyuz-sdk, cargo add fyuz-sdk.

| | | |---|---| | Package | fyuz-sdk | | Version | 0.1.1 | | Runtime | Node ≥ 18 (native fetch), modern browsers, Deno, workers | | Module | ESM only — no CommonJS build | | Types | Ships its own .d.ts; no @types/* package needed | | Dependencies | Zero at runtime |


⚡ Quickstart

import { FyuzClient } from 'fyuz-sdk';

const client = new FyuzClient(); // defaults to https://api.fyuz.fun

// The highest-signal endpoint: one pre-aggregated row per token.
const trending = await client.discover({ tab: 'trending', limit: 5 });
for (const token of trending) {
  const venue = token.launched ? 'PancakeSwap' : 'bonding curve';
  console.log(`${token.symbol.padEnd(8)} $${token.marketcap.toFixed(0).padStart(7)}  ${token.graduationPct.toFixed(1)}% to graduation  (${venue})`);
}

// The biggest token still on the curve. `null` is a real state, not an error.
const king = await client.getKing();
console.log(king === null ? 'no king right now' : `king: ${king.tokenSymbol} @ ${king.marketcap}`);

Real output, run against https://api.fyuz.fun:

PROGI    $   4473  14.9% to graduation  (bonding curve)
TUTACO   $   4473  14.9% to graduation  (bonding curve)
SRIRA    $   4473  14.9% to graduation  (bonding curve)
CROIS    $   4473  14.9% to graduation  (bonding curve)
SLOIN    $   4472  14.9% to graduation  (bonding curve)
king: KIMKACHU @ 4566.671076655321500000

No API key, no signup, no auth header.


🧠 The one thing to understand first

A Fyuz token trades on an internal bonding curve until it hits a $30,000 market cap, then graduates into a PancakeSwap V2 pair.

Before graduation there is no DEX pool anywhere. No PancakeSwap pair, nothing for an aggregator to index. This API is the only source of price data for a pre-graduation token.

| State | pairAddress | launched | Where price lives | |---|---|---|---| | 🌱 On the curve | null | false | price, marketcap, virtualEthAmount, virtualTokenAmount — from this API only | | 🎓 Graduated | the pair address | true | the DEX pair; poolType says which flavour |

poolType is 1 = PancakeSwap V2, 2 = V3, 3 = V4 / direct launch. network is the chain slug — "bsc" in practice. GET /config also lists Robinhood Chain (4663), which currently has no tokens.


🏆 Why this SDK

Four SDKs — TypeScript, Go, Python, Rust — same API, same behaviour. What makes that claim checkable:

| | | |---|---| | Fork-tested against the real contract | scripts/fork-test.sh forks BNB Smart Chain and runs all four suites against the deployed Fyuz proxy 0x33a98bef6496684a8dac83734d9ceb0cefc7019c — not against stubs. | | One conformance record, four languages | Each SDK writes a conformance record on the fork, and fork-test.sh compares them field by field: same quotes, same calldata, same decoded pool state. | | Quotes come from the curve, not from arithmetic | Every quote is an eth_call to getSwapOutput on the live contract. Exact-tokens pricing was verified correct to the wei by bisecting the deployed contract. | | Reverts classified from real revert data | The revert table is driven by payloads the real contract returned — SwapExpired 0x2b32713d, SlippageExceeded 0x8199f5f3 — not by fixtures someone typed. | | Zero runtime dependencies | Native fetch, a 254-line ABI codec (src/abi.ts), a 290-line JSON-RPC client (src/rpc.ts). Nothing else. | | Never signs, never broadcasts | The trade builder hands you { chainId, to, data, value }. Your wallet owns the key; this package cannot move funds. | | 164 hermetic tests | The offline TypeScript suite runs against a loopback node:http stub and never touches the network. (Go 100, Python 177, Rust 130 + 47 doctests.) |


🔢 Precision: decimal fields are strings, and they stay strings

Token amounts carry 18 decimals and wei values are uint256. Neither fits exactly in a JavaScript number, so the API returns them as decimal strings and this SDK keeps them as string.

const stats = await client.distributor.getStats();

stats.totalPaidWei;          // "1250000000000000001"   ✅ exact
BigInt(stats.totalPaidWei);  // ✅ arithmetic that stays exact
Number(stats.totalPaidWei);  //  1250000000000000000    ❌ silently wrong

The rule: anything named *Wei, plus marketcap / price / ethPrice / volume / score / liquidity / tokenAmount / virtualEthAmount / virtualTokenAmount on Token, Holder, TokenDetail and Trade, is a string. Convert to BigInt for integers, or to a big-decimal library for fractional values — and only at the point of display.

DiscoverToken, analytics, portfolio and leaderboard responses return number instead. Those are pre-rounded aggregates; there is no precision left to protect.

⚠️ null never means zero

Pot.potBnb, Pot.totalPoints, Claimable.claimableWei, TokenDetail.curveHolding and the nullable RoundReceipt wei fields are null when the chain read failed or the round is not indexed yet. That is unknown, not zero. Hide the figure; do not render 0.

const pot = await client.distributor.getPot();
console.log(pot.potBnb === null ? 'Pot: —' : `Pot: ${pot.potBnb} BNB`);

📖 API reference

Every method takes an optional trailing RequestOptions (signal, timeoutMs, maxRetries, headers). Timestamps are UNIX seconds unless stated otherwise.

🩺 System

| Method | Endpoint | Returns | |---|---|---| | health() | GET /health | HealthStatus | | getConfig() | GET /config | ChainConfig | | baseUrl (getter) | — | string |

📈 Market data

| Method | Endpoint | Returns | |---|---|---| | discover(params?) | GET /discover | DiscoverToken[] | | listTokens(params?) | GET /tokens | TokenPage | | iterateTokens(params?) | GET /tokens (all pages) | AsyncGenerator<Token> | | iterateTokenPages(params?) | GET /tokens (all pages) | AsyncGenerator<TokenPage> | | getKing() | GET /tokens/king | Token \| null | | getToken(network, address, params?) | GET /tokens/{network}/{tokenAddress} | TokenDetail |

💹 Trades & charts

| Method | Endpoint | Returns | |---|---|---| | getRecentTrades(params?) | GET /trades/recent | RecentTrades | | getTokenTrades(address, params?) | POST /trades | Trade[] | | getChartData(params) | GET /trades/getChartData | Candle[] |

🔬 Analytics & wallets

| Method | Endpoint | Returns | |---|---|---| | getTokenAnalytics(network, address) | GET /analytics/token/{network}/{address} | TokenAnalytics | | getTopTraders(network, address) | GET /analytics/top-traders/{network}/{address} | TopTrader[] | | getWallet(address) | GET /wallet/{address} | WalletStats | | getPortfolio(address) | GET /portfolio/{address} | Portfolio |

🥇 Leaderboards & profiles

| Method | Endpoint | Returns | |---|---|---| | getUserLeaderboard(params?) | GET /users/leaderboard | LeaderboardEntry[] | | getUserProfile(address) | GET /users/profile/{address} | UserProfile | | getTopHolders(count, params?) | GET /users/top/{count} | TopHolderEntry[] | | getKingsHistory() | GET /kings/history | KingReign[] | | getSeason() | GET /season | Season | | getReferralLeaderboard(params?) | GET /referrals/leaderboard | ReferralLeaderEntry[] | | getTier(address) | GET /tier/{address} | TierInfo | | getRewards(address) | GET /rewards/{address} | Rewards |

💰 Distributor — client.distributor

| Method | Endpoint | Returns | |---|---|---| | getStats() | GET /distributor/stats | PayoutStats | | getPot() | GET /distributor/pot | Pot | | getShares(params?) | GET /distributor/shares | Shares | | listRounds(params?) | GET /distributor/rounds | RoundReceipt[] | | getRound(id) | GET /distributor/rounds/{id} | RoundDetail | | getPayouts(address) | GET /distributor/payouts/{address} | AddressPayouts | | getClaimable(address) | GET /distributor/claimable/{address} | Claimable |

🔧 Trading — client.trade

| Method | What it does | Returns | |---|---|---| | chain(network?) | Resolve contract address, chain id, RPC from GET /config | ResolvedChain | | poolState(params) | Curve reserves + graduation flag, straight from tokenPools | PoolState | | isGraduated(params) | The contract's own launched flag | boolean | | firstBuyFee(params) | getFirstBuyFee(token), wei | string | | maxSellableWei(params) | getMaxSellableETH(token), wei | string | | allowance(params) | ERC-20 allowance(owner, contract) | string | | balanceOf(params) | ERC-20 balanceOf(owner) | string | | quoteBuy(params) | Price a buy via getSwapOutput | BuyQuote | | quoteSell(params) | Price a sell via getSwapOutput | SellQuote | | buildBuy(params) | Unsigned swapExactETHForTokens | BuiltTrade<BuyQuote> | | buildBuyExactTokens(params) | Unsigned swapETHForExactTokens | BuiltTrade<BuyQuote> | | buildSell(params) | Unsigned swapExactTokensForETH | BuiltTrade<SellQuote> | | buildApprove(params) | Unsigned ERC-20 approve | UnsignedTransaction |

Unit conversion & ABI helpers — from abi.ts, all exact, no float anywhere:

| Export | Purpose | |---|---| | parseUnits(value, decimals = 18) | "0.5""500000000000000000", string in, string out | | formatUnits(wei, decimals = 18) | The exact inverse; handles negative bigint correctly | | parseWeiAmount(name, value) | Validate a base-10 wei string into a bigint; rejects decimal points | | parseWeiAmountOrNull(name, value) | Same, but null/undefined pass through | | normalizeAddress(name, value) | Validate a 20-byte address and lower-case it | | UINT256_MAX, WEI_PER_BNB | 2n ** 256n - 1n, 10n ** 18n |

Address bookKNOWN_DEPLOYMENTS, findDeployment(chain), fyuzContractAddress(chain), types Deployment / DeploymentContracts.

TransportRpcClient, DEFAULT_RPC_TIMEOUT_MS, MAX_RPC_RESPONSE_BYTES, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_BASE_DELAY_MS, DEFAULT_RETRY_MAX_DELAY_MS, types FetchLike / FyuzClientOptions / RequestOptions / EthCallParams / RpcCallOptions / RpcClientOptions.

PaginationDEFAULT_PAGE_SIZE, MAX_PAGE_SIZE (both 100).

TradingTradeClient, DEFAULT_DEADLINE_SECONDS (120), DEFAULT_NETWORK ('bsc'), revertError, revertSelector, types BuyOptions / BuyExactTokensOptions / SellOptions / BuyQuote / SellQuote / BuiltTrade / UnsignedTransaction / PoolState / ResolvedChain / TradeClientOptions.

Errors — every class below, plus isFyuzError, isRateLimitError, type RevertKind.

VersionVERSION, USER_AGENT (fyuz-sdk-typescript/0.1.1).

Parameter typesDiscoverParams, DiscoverTab, ListTokensParams, SortDirection, TokenStatus, TokenDetailParams, RecentTradesParams, TokenTradesParams, ChartDataParams, LeaderboardParams, TopHoldersParams, SharesParams, ListRoundsParams.

Response models — every wire type is a named interface: Token, TokenPage, TokenDetail, DiscoverToken, Holder, KingResponse, Trade, TradeSide, RecentTrades, Candle, TokenAnalytics, TopTrader, WalletStats, Portfolio, PortfolioPosition, UserProfile, UserSummary, ChatMessage, TopHolderEntry, TierInfo, TierName, QuestState, AchievementState, Rewards, LeaderboardEntry, ReferralLeaderEntry, KingReign, Season, SeasonEntry, PayoutStats, Pot, Shares, ShareEntry, RoundReceipt, RoundDetail, PayoutLine, AddressPayouts, Claimable, ChainInfo, ChainConfig, HealthStatus, CreatorInfo, ApiErrorBody.

  • discover is the endpoint to build an indexer on: market cap, 24h volume, buy/sell counts, holder count, 24h price change and graduation progress in one row per token, one round trip.
  • getKing() returns null when the hill is empty. That is a real state — a token leaves the hill the moment it graduates, so between a graduation and the next launch there is no king. The API's {"king": null} envelope is unwrapped for you.
  • getRecentTrades supports incremental polling: pass the highest id you have already seen as latestTradeId and you get only what is new.
  • getTokenTrades is POST /trades because the token address travels in the body. It is a read, not a mutation, and needs no authentication.
  • getToken's curveHolding is null when the on-chain read failed — that means unknown, not zero.
  • Leaderboard rank is a server-assigned 1-based ordinal matching the returned order. Do not re-sort client-side; the ordering key is not always the column you are displaying.
  • distributor.getRound returns the receipt fields flattened alongside payouts — read round.potWei, not round.round.potWei.
  • Timestamps: Token.createdAt / updatedAt / launchedAt / creationTime and ChatMessage.date are RFC-3339 strings. Everything else (from, to, roundEnd, Trade.date, DiscoverToken.createdAt, …) is UNIX seconds.

A slice of every trade fee accumulates in the Distributor contract. At the end of each round most of the pot is paid out pro-rata by leaderboard points, and the remainder goes to a single Chainlink-VRF-picked winner. getPot() shows the live undistributed pot, getShares() shows the allocation the round-runner will post on-chain — including the exact postShares calldata in Shares.packed — and listRounds() / getRound() are the public ledger of every wei already paid. getClaimable() covers the rare case where a push payment failed and the wallet must call claim() itself. This SDK only reads. Claiming is an on-chain call your wallet signs.


🚨 Errors, rate limits and retries

The API allows 120 requests/minute per IP and answers 429 {"error": "Too many requests"} beyond that. The client retries 429, 5xx and connection failures automatically with exponential backoff and full jitter, honouring Retry-After when present — on a 503 from a draining load balancer as readily as on a 429. The header can only lengthen the wait (a Retry-After: 0 from a proxy cannot spin the retry loop) and is clamped to retryMaxDelayMs. Timeouts, aborts and every other 4xx surface immediately: retrying those would only burn the caller's deadline.

import { FyuzRateLimitError, FyuzNotFoundError, FyuzApiError, FyuzError } from 'fyuz-sdk';

try {
  await client.getUserProfile('0x0000000000000000000000000000000000000001');
} catch (err) {
  if (err instanceof FyuzRateLimitError) {
    console.warn(`rate limited, retry after ${err.retryAfterSeconds ?? '?'}s`);
  } else if (err instanceof FyuzNotFoundError) {
    console.warn('no profile for that address');
  } else if (err instanceof FyuzApiError) {
    console.error(err.status, err.message); // message is the server's {"error": …} envelope
  } else if (err instanceof FyuzError) {
    console.error('transport failure', err.message);
  } else {
    throw err;
  }
}

| Error | Thrown when | Extends | |---|---|---| | FyuzApiError | Any non-2xx. Carries status, method, url, body, retryAfterSeconds | FyuzError | | FyuzRateLimitError | HTTP 429 | FyuzApiError | | FyuzNotFoundError | HTTP 404 — no such token, round or profile | FyuzApiError | | FyuzTimeoutError | The per-attempt timeout elapsed | FyuzError | | FyuzConnectionError | Never reached the API: DNS, refused, TLS, dropped socket | FyuzError | | FyuzParseError | A 2xx body that was not valid JSON — usually a proxy in the way | FyuzError | | FyuzInvalidArgumentError | An argument failed validation before anything was sent | FyuzError | | FyuzRpcError | A JSON-RPC node errored, or answered with something that was not JSON-RPC | FyuzError | | FyuzContractRevertError | The contract rejected a simulated call. Carries kind, errorName, selector, data, retryable | FyuzError | | FyuzTokenGraduatedError | The token left the curve; every swap entrypoint reverts AlreadyLaunched() | FyuzContractRevertError | | FyuzError | Base class for everything above | Error |

Two type guards — isFyuzError(value) and isRateLimitError(value) — cover the same ground without instanceof.

A reverted eth_call comes back as a bare 4-byte selector. The SDK maps it to a RevertKind and a message you can act on. retryable says whether re-quoting and retrying could plausibly succeed.

| kind | Solidity error | Retryable | |---|---|---| | graduated | AlreadyLaunched() 0xcfa6d878 | no | | no_pool | PoolDoesNotExist() 0x9c8787c0 | no | | slippage | SlippageExceeded() 0x8199f5f3, MaxInputExceeded() 0xde9d3c88 | yes | | expired | SwapExpired() 0x2b32713d | yes | | price_impact | MaxPriceImpactExceeded() | no | | sell_too_large | SellAmountTooLarge() | no | | insufficient_value | InsufficientEthValue() | no | | insufficient_balance | InsufficientTokenBalance() | no | | insufficient_input | InsufficientInput() | no | | insufficient_liquidity | InsufficientLiquidity(), InvalidOutput() | no | | zero_amount | ZeroAmount() | no | | stale_feed | StalePriceFeed() | yes | | paused | EnforcedPause() | yes | | revert_string | Error(string) — the reason is decoded into the message | no | | unknown | An unrecognised selector, a Panic(uint256), or a node that stripped the revert data | no |

An unrecognised selector is reported verbatim rather than swallowed, so a contract upgrade that adds an error still leaves you something to search for.


📄 Auto-pagination

GET /tokens is the one paginated endpoint. Async generators walk it for you, fetching pages lazily — break and no further request is made.

// One token at a time.
for await (const token of client.iterateTokens({ orderType: 'marketcap', orderFlag: 'desc' })) {
  console.log(token.tokenSymbol, token.marketcap); // marketcap is a decimal STRING
  if (token.id === 500) break;                     // stops fetching immediately
}

// Or page by page.
for await (const page of client.iterateTokenPages({ pageSize: 100 })) {
  console.log(`${page.tokenList.length} of ${page.tokenCount}`);
}

Iteration stops on a short page, an empty page, or once tokenCount rows have been seen — whichever comes first, so a stale count can never spin forever. pageSize defaults to 100 (MAX_PAGE_SIZE, the server's ceiling) and is clamped before it goes on the wire, so "short page" means what it says.


⚙️ Configuration

const client = new FyuzClient({
  baseUrl: 'https://api.fyuz.fun', // default
  timeoutMs: 30_000,               // per attempt, default 30s
  maxRetries: 3,                   // default 3; 0 disables retrying
  retryBaseDelayMs: 250,           // first backoff window
  retryMaxDelayMs: 8_000,          // ceiling on any single sleep
  headers: { 'x-app': 'my-indexer' },
  userAgent: 'my-indexer/2.1',     // default: fyuz-sdk-typescript/0.1.1
  fetch: myInstrumentedFetch,      // defaults to the global fetch
  trade: { rpcUrl: process.env.BSC_RPC_URL },
});

Non-finite numeric options (NaN from a parseInt on an unset env var, say) fall back to the documented default rather than corrupting the retry loop.

Every method takes the same knobs per call as its last argument, plus an AbortSignal:

const controller = new AbortController();
setTimeout(() => controller.abort(), 1_000);

await client.discover({ tab: 'new' }, { signal: controller.signal, maxRetries: 0 });

🌐 Browser use

Works unchanged. User-Agent is a forbidden header name in browsers, so the SDK's identifying header is dropped there — nothing else changes. Only getTokenTrades (POST /trades) triggers a CORS preflight, because its content-type: application/json is not a safelisted value; every other call is a simple request. Either way, browser use requires your origin to be on the API's allow-list.


💱 Trading — unsigned transactions only

client.trade builds unsigned bonding-curve transactions. It never sees a private key, never signs and never broadcasts. It hands you { chainId, to, data, value } and you pass that to whatever wallet you already have — viem, ethers, a hardware signer, a multisig UI.

🔐 This is a security property, not a limitation. Signing is the part that needs a crypto library, and it is not in this package. That is also why fyuz-sdk still has zero runtime dependencies.

import { FyuzClient, parseUnits, formatUnits } from 'fyuz-sdk';

const token = '0x42322852a918f94186b7dfda2e0e3f4ad3528480';
const client = new FyuzClient({ trade: { rpcUrl: process.env.BSC_RPC_URL } });

const quote = await client.trade.quoteBuy({ token, amountWei: parseUnits('0.05') });
console.log(`${formatUnits(quote.amountOutWei)} tokens for ${formatUnits(quote.valueWei)} BNB`);

const built = await client.trade.buildBuy({
  token,
  amountWei: parseUnits('0.05'),
  slippageBps: 100, // 1% — required, there is no default
});

await walletClient.sendTransaction(built.transaction); // your wallet, your key

Real output against the live curve and the deployed contract:

contract   0x33a98bef6496684a8dac83734d9ceb0cefc7019c
launched   false
amountIn   0.05 BNB
fee        0 BNB
msg.value  0.05 BNB
tokens out 6266097.429591425272749449
impact bps 59
to         0x33a98bef6496684a8dac83734d9ceb0cefc7019c
value      0xb1a2bc2ec50000
data[0:10] 0x6bf05b01          ← swapExactETHForTokens
minOut     6203436.455295511020021954

💸 What msg.value has to be

msg.value is amountIn + getFirstBuyFee(token) — the fee rides on top of the swap amount, it is not deducted from it. transaction.value already includes it; sending only amountWei reverts with InsufficientEthValue(). Any excess is refunded by the contract. BuyQuote spells all three out:

| Field | Meaning | |---|---| | amountInWei | What goes into the curve | | firstBuyFeeWei | getFirstBuyFee(token) — charged on top | | valueWei | What msg.value must be | | amountOutWei | Tokens out at the quoted moment | | priceImpactBps | Price impact, as the contract computes it |

Separately, the contract's own gross-up on the input side of a buy is PLATFORM_BUY_FEE_BPS 80 + TOKEN_OWNER_FEE_BPS 20 = 100 bps. The SDK reads both rather than hardcoding them, because both are plain storage with owner setters.

🎚️ Slippage is never chosen for you

buildBuy, buildBuyExactTokens and buildSell require either slippageBps or an explicit limitWei. There is no default — a default is a number picked by someone who cannot see the trade, and being wrong costs you money.

{ slippageBps: 100 }            // 1% tolerance, applied to the live quote
{ limitWei: '6200000000…' }     // exact minAmountOut / maxAmountIn, your number

Deadlines default to DEFAULT_DEADLINE_SECONDS (120s from now); pass deadlineSeconds or an absolute deadline to override.

✍️ Selling needs an approval first

The contract pulls tokens with transferFrom, so an ERC-20 allowance must already be in place or the swap reverts inside the token.

const allowance = await client.trade.allowance({ token, owner });

if (BigInt(allowance) < BigInt(amountWei)) {
  const approve = await client.trade.buildApprove({ token, amountWei });   // or { unlimited: true }
  await walletClient.sendTransaction(approve);
}

const sell = await client.trade.buildSell({ token, amountWei, slippageBps: 150 });
await walletClient.sendTransaction(sell.transaction);

🔄 Nothing is hardcoded on the trading path

Contract address, chain id and RPC endpoint all come from GET /config at runtime, resolved once per client and memoised. A proxy upgrade or a brand-new chain therefore needs no SDK release. Both values are validated before they reach anything you sign: contractAddress is the to of every trade and the spender of every approval, and chainId is what stops a BSC-signed transaction replaying elsewhere.

⚠️ Pass your own rpcUrl in production. The endpoint /config publishes belongs to the API operator, is shared by every caller, and can change or start refusing requests without notice.

A token that has graduated raises FyuzTokenGraduatedError instead of building a transaction that would revert — the SDK refuses rather than letting you pay gas to learn it on-chain.

Every other quote here is an eth_call, deliberately: reimplementing the curve locally would mean a second formula that has to be kept in step with an upgradeable contract, and the day they disagree is the day someone's minAmountOut is wrong.

buildBuyExactTokens cannot be. getSwapOutput answers "what do I get for X" in both directions, and the contract publishes no view at all for "what does Y cost". Pricing off getSwapOutput(wanted, isETHInput=false) is not an approximation of the right answer — it is the wrong side of the curve, and it under-funds maxAmountIn badly enough that swapETHForExactTokens reverts MaxInputExceeded after you have paid the gas.

So this one mirrors Fyuz.getAmountIn exactly — including its round-up against the buyer and its fee gross-up on the input — over reserves and fee rates read from the same contract. The inputs are still the contract's; only the multiply is local. It was verified correct to the wei by bisecting the deployed contract on a fork.


🗺️ Deployed addresses

Chain addresses ship as a frozen constant, not a JSON file read at runtime: a published package cannot rely on a readable file next to the module — not in a browser, a bundler, Deno, or a Lambda whose working directory is elsewhere. The test suite asserts the constant against shared/addresses/*.json, so the copy cannot drift.

import { fyuzContractAddress, findDeployment, KNOWN_DEPLOYMENTS } from 'fyuz-sdk';

fyuzContractAddress(56);           // '0x33a98bef6496684a8dac83734d9ceb0cefc7019c'
fyuzContractAddress('bsc');        // same — id, slug or numeric-string all work
findDeployment(56)?.explorerUrl;   // 'https://bscscan.com'
KNOWN_DEPLOYMENTS.length;          // 2

The point is that a caller never types an address. The trading path still prefers GET /config — the contract is behind an upgradeable proxy, and a pinned address in a published package cannot be fixed after a redeploy. The book is the fallback and the reference: it fills in a chain /config does not serve, and it answers offline questions like "what do I fork against".


🧪 Testing

pnpm test          # 164 tests, hermetic — a loopback node:http stub, never the network
pnpm typecheck     # strict type-check, no emit

Two layers:

1. Offline (pnpm test). 164 tests against a local node:http stub server on a loopback port. No network, deterministic, runs in about a second. This layer proves the SDK is self-consistent: URL building, retry and backoff behaviour, Retry-After handling, error mapping, pagination termination, decimal-string preservation, ABI encoding against pinned golden calldata, revert classification.

2. Fork (../scripts/fork-test.sh). Forks BNB Smart Chain with anvil and runs the same suites against the real deployed Fyuz proxy. This layer proves the SDK agrees with the contract: quotes are checked against getSwapOutput on the live curve, built calldata is eth_call'd against the real proxy, and revert classification is driven by revert data the contract actually produced.

Each SDK writes a conformance record on the fork, to .fork-out/<language>.json. All four — TypeScript, Go, Python, Rust — agree on every field of it: same quotes, same calldata, same decoded pool state. fork-test.sh checks this by flattening each record and comparing leaf by leaf (it computes no digest; the files differ on disk only in JSON key order). That is what makes "same API, same behaviour" a checkable claim rather than a slogan. Shared fixtures in shared/test-vectors/ back all four suites, so a change to the wire contract fails every language at once.


📂 Examples

Runnable programs in examples/. Each has a sibling in the Go, Python and Rust SDKs.

| Example | What it shows | |---|---| | 01-quickstart.ts | Health, chain config, the trending feed, king of the hill | | 02-graduation-watch.ts | Tokens closest to the $30k graduation threshold | | 03-token-deep-dive.ts | One token: detail, holders, trades, hourly candles | | 04-export-tokens.ts | Auto-paginate every token to CSV with exact decimals | | 05-resilient-polling.ts | Incremental trade polling, retries, error classification | | 06-trade.ts | Quote a buy and a sell, build the unsigned transactions, handle the approval |

pnpm build:examples
node build/examples/01-quickstart.js
node build/examples/04-export-tokens.js dog > tokens.csv
node build/examples/06-trade.js            # signs nothing, sends nothing

🌍 The same SDK in four languages

Same API, same behaviour, same fork-verified calldata.

| | Package | Install | Runtime dependencies | |---|---|---|---| | 🟦 TypeScript | fyuz-sdk | pnpm add fyuz-sdk (not on the registry yet) | none — native fetch | | 🐹 Go | github.com/fyuz-launchpad/fyuz-sdk/go | go get github.com/fyuz-launchpad/fyuz-sdk/go | nonenet/http | | 🐍 Python | fyuz-sdk (import fyuz) | pip install fyuz-sdk | none — stdlib urllib | | 🦀 Rust | fyuz-sdk (crate fyuz-sdk, lib fyuz) | cargo add fyuz-sdk | reqwest, serde, serde_json, thiserror, tokio — deliberate |

All four are at 0.1.1. See ../README.md for the side-by-side.


🛠️ Development

pnpm install
pnpm build           # pnpm clean && tsc -> dist/ with .d.ts declarations
pnpm typecheck       # strict type-check without emitting
pnpm test            # compiles src+test+examples -> build/, then node --test
pnpm build:examples  # compile examples/ -> build/examples/

This project is pnpm-only.


📜 License

MIT · fyuz.fun · github.com/fyuz-launchpad/fyuz-sdk