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

@letscashfun/sdk

v0.4.0

Published

TypeScript SDK for the letscash.fun launchpad — launch tokens, read and claim fee streams, and trade, on Robinhood Chain

Readme

@letscashfun/sdk

TypeScript SDK for the letscash.fun launchpad on Robinhood Chain.

Launch tokens, read and claim fee streams, transfer stream ownership, trade, and run the permissionless keeper jobs.

npm install @letscashfun/sdk viem

Requirements: Node 20 or later, and viem 2.21 or later as a peer dependency. The package has no runtime dependencies of its own and runs no install scripts.

  • Cookbook — recipes for every launch shape, claim form and splitter operation
  • Security — key handling, supply chain, and reporting a vulnerability

Quickstart

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { LetscashClient, robinhoodChain } from "@letscashfun/sdk";

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const client = new LetscashClient({
  publicClient: createPublicClient({ chain: robinhoodChain, transport: http() }),
  walletClient: createWalletClient({ account, chain: robinhoodChain, transport: http() }),
});

const config = await client.selectConfig({ quote: "ETH", feePercent: 1, supplyTokens: 1_000_000_000 });

const { token, poolId } = await client.launch({
  configId: config.id,
  name: "My Coin",
  symbol: "MINE",
});

const coin = await client.token(token);
console.log((await coin.fees.claimable()).toString()); // "0.42 ETH"
await coin.fees.claim();

Reads require no wallet:

const client = new LetscashClient({ publicClient });
const coin = await client.token("0xfd45…");
await coin.fees.claimable();

Signing is performed entirely by the viem WalletClient you supply. The SDK never handles a private key.


How it works

A launch mints a fixed-supply ERC-20 and initialises a Uniswap v4 pool quoted in ETH or USDG, seeded single-sided from the full supply. The pool carries no LP fee; a custom hook takes a configurable fee on the quote leg of every trade and accrues it per pool.

That fee stream is the part most integrations are built around. It can pay the creator, be split across up to four addresses, or be routed into a self-burner that buys and burns the token. Ownership of a stream can be transferred at any time.

The launch menu

Every launch selects a published configuration. Configurations are immutable once published, can be enabled or disabled without a redeployment, and are added over time — so read the menu rather than hardcoding an id.

const enabled = await client.getConfigs();
const usdg = await client.getConfigs({ quote: "USDG" });
const burn = await client.getConfigs({ selfBurn: true });
const all = await client.getAllConfigs();   // including disabled rows

Each row exposes the raw contract fields plus derived values:

config.feePercent               // 1 | 3 | 5 | 10
config.creatorPercentOfVolume   // 0.7 on a 1% pool
config.platformPercentOfVolume  // 0.3 on every tier
config.supplyTokens             // 1_000_000_000
config.quote                    // { address, symbol, decimals }

Fee streams

const coin = await client.token(tokenAddress);

await coin.fees.claimable();     // what a claim pays out, now
await coin.fees.tab();           // swept and banked
await coin.fees.pendingGross();  // unswept, before the creator/platform split
await coin.fees.creator();       // current owner of the stream

await coin.fees.claim();                  // to the caller
await coin.fees.claimTo(address);         // to a named address
await coin.fees.claimAmount(to, amount);  // a partial claim

claim sweeps internally, so no separate sweep is required.

Transferring a stream is irreversible and carries the unclaimed balance with it:

await coin.fees.claim();
await coin.fees.transferTo(newOwner);

Event subscriptions are available for fee accrual, claims and new launches:

const unsubscribe = coin.fees.onFeeAccrued((fee) => console.log(fee.toString()));
client.watchLaunches(({ token, poolId, creator }) => { /* … */ });

Trading

const quote = await coin.trade.getQuote("buy", Amount.parse("0.1", coin.quote));

await coin.trade.buy(Amount.parse("0.1", coin.quote), { slippageBps: 100 });
await coin.trade.sell(Amount.parse("1000000", coin.asset), { slippageBps: 100 });

Permit2 approvals are granted automatically when the settled asset is an ERC-20. Note this includes buys on a USDG pool, not only sells.

Split fee streams

const { splitter } = await client.launch({
  configId: config.id,
  name: "My Coin",
  symbol: "MINE",
  feeRecipients: [
    { address: alice, shareBps: 5000 },
    { address: bob,   shareBps: 3000 },
    { address: carol, shareBps: 2000 },
  ],
});

Shares must sum to 10000 and are fixed at launch — they cannot be changed by the creator or by the platform. A maximum of four recipients is supported; a single recipient names that address directly and deploys no splitter.

From a recipient's side:

const s = await coin.splitter();     // null when the stream is not split
await s.collectable(myAddress);      // includes fees still held at the hook
await s.collect();
await s.rotate(newAddress);          // transfers the slot, irreversibly

A recipient can only move their own balance. distribute is the sole permissionless write.

Custom supply

Not live yet. The factory upgrade that accepts this has not been executed. Calling it against the current deployment throws before anything is signed, rather than quietly minting the row's supply. The SDK detects which factory is live and encodes for it, so this version is correct on both sides — you can ship against it today and the feature starts working when the upgrade lands.

const config = await client.selectConfig({ quote: "ETH", feePercent: 1, supplyTokens: 1_000_000_000 });
await client.launch({ configId: config.id, name: "My Coin", symbol: "MINE", supply: 250_000_000_000 });

Any whole number of coins from one billion to one quadrillion — named against the one-billion row, and only that row. The opening tick is derived relative to the config, and the larger published rows carry their own drift, so the same supply elsewhere would open at a different valuation. Fractional supplies are refused: every display renders whole coins.

It does not change the opening valuation — the factory derives the tick from the size you name — so supply is a choice about the price per coin. Omit it for the row's own figure.

Airdrop vaults

A launch can route part of its first buy — not part of supply — into a vault it cannot withdraw from.

const { airdropVault } = await client.launch({
  configId: config.id,
  name: "My Coin",
  symbol: "MINE",
  firstBuy: Amount.parse("2", ETHER),
  airdropBps: 3000,                  // 30% of the buy goes to the vault
});

const vault = client.airdropVault(airdropVault!);

// Build the campaign and check it against chain state in one step.
const plan = await vault.prepare({
  recipients,                        // [{ address, amount }], any order
  targetToken,                       // whose holders these are
  snapshotBlock, snapshotBlockHash,
});

await vault.publish(plan.campaign);  // opens, then every remaining part
// …five minutes…
await vault.executeAll(plan.campaign);

The tokens leave along the published list, or they burn seven days after the launch. There is no third outcome and no withdrawal function.

This is a notice requirement, not a sale lock. A creator may publish a list naming one wallet they control and pay themselves once the countdown elapses. That cannot be prevented by any contract, and a product describing it as a lock would be selling a guarantee it does not have. What the vault removes is doing it quietly: the destination, the amounts, the recipient count and the countdown are all on chain before a single token moves.

Two more things worth knowing before rendering one:

  • Record, not notice. No view is keyed by recipient address and no event indexes the people, so nobody is told they are on a list — the countdown is something anyone can watch. CampaignSealed is the one event a watcher needs.
  • A completed campaign clears itself, and the remainder can be declared in a new one. The first list a vault publishes is not necessarily the destination of everything it holds. Show the balance next to the campaign.

publish and executeAll both resume from chain state, so a run that dies half way can simply be run again. Execution is permissionless: a stranger can finish an airdrop the creator walked away from, and cannot alter one byte of who gets what.

Building a campaign is pure — no node, no wallet, no clock — so it works in a browser or a worker:

import { allocateProRata, buildCampaign } from "@letscashfun/sdk";

const { recipients, remainder } = allocateProRata({ holders, total, vault: vaultAddress });
const campaign = buildCampaign({ vault: vaultAddress, chainId, campaignId, recipients, ... });

allocateProRata is the allocation rule written down — floor pro rata, drop dust, leave the remainder in the vault — so a second implementation reproduces the same list to the wei rather than guessing at it. It excludes the Uniswap PoolManager by default; on a letscash token the pool holds everything nobody has bought yet, and paying it sends the airdrop into the pool's reserves.

Keeper jobs

const burner = await client.selfBurner();
await burner.burn(poolId);          // permissionless, pays a bounty

const converter = client.revenueConverter();
await converter.convert(usdg);      // permissionless, pays no bounty

Protocol behaviour to be aware of

Five aspects of the protocol are not evident from the ABI and are handled by the SDK.

Pool identity. A Uniswap v4 pool is identified by the hash of a five-field key. An incorrect quote, tick spacing or hook produces a well-formed identifier for a pool that does not exist, and every subsequent read returns zero rather than an error. client.token(address) reads the identifier stored on the token, rebuilds the key, and verifies the two agree.

Launch salts. The factory rejects any salt whose resulting token address does not carry the cc suffix and sort above the quote asset. Salts must be obtained from mineSalt, a read-only search that succeeds after roughly a thousand attempts and can exhaust a window. client.launch() performs the search and retries.

Fee accounting. tab holds the creator's share after the split; pending holds the gross fee before it. Summing them overstates the claimable balance by the platform's share — 30% on a 1% pool. fees.claimable() applies the split correctly.

Token metadata. Trading terminals hyperlink social fields verbatim, so a bare handle is not rendered as a link. buildTokenMetadata() produces the same document letscash.fun publishes, with handles expanded to complete URLs. Pinning is performed with your own IPFS credential; see the cookbook.

Decimals. ETH pools settle in 18 decimals and USDG pools in 6. Every value returned by the SDK is an Amount carrying its own scale, and arithmetic across two different assets throws.

Airdrop campaign hashing. The vault stores a fingerprint of the recipient list, not the list, and every part and batch proves against it. A tree built even slightly differently publishes fine and then cannot pay anybody — the campaign seals, the countdown runs, every execute reverts, and the supply burns. The vault promotes an odd leaf where most libraries duplicate it, and every leaf binds the chain, the vault, the campaign id and the index. buildCampaign mirrors all of it, and the test suite checks its output against the deployed contract's own partLeaf, batchLeaf and published part roots rather than against a fixture.

Two dialects at one address. The factory is a proxy, and the upgrade that lets a launch name its own supply adds a field to TokenParams — moving the selector of launch, launchWithPermit, launchWithFeeSplit, launchWithAirdrop, mineSalt and predictTokenAddress while the address stays put. Airdrop vaults have the same split: Declaration gained amountScale, so openCampaign and campaign() moved, and vaults deployed earlier are immutable clones that keep the old shape forever. The SDK asks the chain which is live — once per client for the factory, once per vault for the campaign ABI — and encodes to match, so one version works on both sides of the upgrade.

Two block numbers. This chain is an Arbitrum Orbit L2, so the EVM's NUMBER opcode returns the parent chain's height while every RPC is indexed by the L2 height. A campaign's snapshotBlock must be the L2 one. Quoting the wrong namespace is refused on chain — and the check was originally written the other way round, where it accepted only fabricated snapshots.


Errors

Contract reverts are decoded into a ContractRevertError carrying the Solidity error name and guidance for that specific error:

import { ContractRevertError } from "@letscashfun/sdk";

try {
  await coin.fees.claim();
} catch (error) {
  if (error instanceof ContractRevertError && error.errorName === "NotCreator") {
    // stream ownership has been transferred
  }
}

Writes are simulated before signing, so a revert is reported before any gas is spent.


Consuming from other languages

ABIs are generated from the compiled contracts and published in three forms:

| Path | Format | |---|---| | @letscashfun/sdk/abis | const-asserted TypeScript, for viem type inference | | abis/*.json | plain JSON, for any language | | solidity/*.sol | curated interfaces for integrating contracts |

The Solidity interfaces cover the functions an integrator calls, rather than the full deployed surface. Their selectors are verified against the generated ABIs in the test suite.

import { ILetscashHook } from "@letscashfun/sdk/solidity/ILetscashHook.sol";
import { IAirdropVault } from "@letscashfun/sdk/solidity/IAirdropVault.sol";

contract MyStrategy {
    function harvest(ILetscashHook hook, bytes32 poolId) external {
        hook.claim(poolId, treasury);
    }

    /// Anyone may finish an airdrop. The caller cannot change who gets what.
    function finish(IAirdropVault vault, uint32 part, uint32 batch, bytes calldata entries, bytes32[] calldata proof)
        external
    {
        vault.execute(part, batch, entries, proof);
    }
}

Contract addresses

Robinhood Chain, chain id 4663.

| Contract | Address | |---|---| | Factory (UUPS proxy) | 0x5bd1Fbe78a78fe8236fa00CF48fbEBA74ae34661 | | Hook | 0x75A54357D9C78a2Db19004a5FDc76c50F9242AEC | | Pool manager | 0x8366a39CC670B4001A1121B8F6A443A643e40951 | | USDG | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |

All contracts are verified on Blockscout. The factory address is stable across upgrades; the hook and other modules are read from it at runtime rather than hardcoded, so a future release does not require an SDK update.

Per-launch contracts — fee splitters and airdrop vaults — are clones with no fixed address. Read them off the factory (client.airdropVaultFor(poolId), coin.splitter()) rather than recording one, and never trust an address handed to you by a third party: the factory is the only authority on which clone belongs to which pool.


Development

npm install
npm run typecheck
npm run test:unit
npm run build

The end-to-end suite runs against a local anvil fork and is skipped when no fork is reachable:

npm run anvil        # in one terminal
npm run test:e2e     # in another

ABIs are generated, never edited by hand. After a contract change, run forge build in the contract repository and then npm run sync:abis.


Licence

MIT