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

restake

v5.4.0

Published

Automated validator management CLI for [Swarm](https://ethswarm.org/) Bee nodes on Gnosis Chain. Performs staking, token transfers, postage batch top-ups, and wealth redistribution across a set of local Bee nodes.

Readme

staker

Automated validator management CLI for Swarm Bee nodes on Gnosis Chain. Performs staking, token transfers, postage batch top-ups, and wealth redistribution across a set of local Bee nodes.

One invocation is one tick — the process performs at most a single action and exits. Scheduling is external; run it from a cronjob.

What it does

Each invocation:

  1. Queries all configured Bee nodes (at http://localhost:1633 through http://localhost:1632+n) for wallet balances (BZZ and xDAI) and redistribution freeze state
  2. Filters to eligible nodes (BZZ balance at or above --bzz, not frozen). If none are eligible, it reports the fleet's total BZZ and its highest single-node balance to Telegram — so a quiet tick still says how far the fleet is from the --bzz threshold — and exits without acting.
  3. Picks a random eligible node. If that node holds less than 0.1 xDAI, it swaps --bzz BZZ for xDAI on SushiSwap and the tick ends — a node without xDAI cannot pay gas for anything else. Otherwise it picks one of four actions with equal probability:
    • Postage top-up — tops up a random configured batch, unconditionally.
    • Stake — deposits BZZ as stake on the picked node.
    • Aid — transfers BZZ to a random other managed node, so it can stake later.
    • External transfer — sends BZZ to the configured external wallet.
  4. If the chosen action turns out to have nothing to do, the invocation ends without a fallback; the next tick rolls again.
  5. Reports the result (success or failure) to a Telegram chat, then exits

Self-sustaining

Nodes spend xDAI on gas but earn BZZ, so left alone they eventually run dry and stop being able to transact. The gas check above closes that loop: whenever a picked node is below MIN_XDAI_BALANCE (0.1 xDAI), it always converts --bzz BZZ into native xDAI instead of doing a random action.

The swap goes through the SushiSwap V3 BZZ/WXDAI pool on the 0.3% fee tier — SushiSwap has no V2 pair for BZZ on Gnosis. It is two transactions, the first confirmed before the second is sent:

  1. approve the SushiSwap router for --bzz BZZ, skipped when the allowance already suffices
  2. A single multicall of exactInputSingle (BZZ → WXDAI, output kept by the router) followed by unwrapWETH9 (WXDAI → native xDAI, forwarded to the node's own wallet), so the node ends up with the native token it pays gas in

amountOutMinimum is quoted from the pool's slot0 spot price less the pool fee and SWAP_SLIPPAGE_PERCENT, so no quoter contract is needed. Quoting off spot works because a swap of this size barely moves the price: measured against live pool state, --bzz 10 comes out 0.58% under raw spot (the 0.3% fee plus ~0.28% impact), leaving ~4.4pp of headroom under the 5% guard. The margin only gets tight around 100 BZZ per swap — raise SWAP_SLIPPAGE_PERCENT before going that big, or check the pool's liquidity first.

At --bzz 10 a swap yields roughly 0.4 xDAI, about 4× MIN_XDAI_BALANCE, so one swap lifts a node clear of the threshold and it does not re-trigger on the next tick. With a much smaller --bzz the node would instead swap on several consecutive ticks until it built up a buffer.

Prerequisites

  • Node.js 18+
  • pnpm
  • n Bee nodes running locally on consecutive ports starting at 1633
  • A Gnosis Chain RPC endpoint
  • A Telegram bot token and chat ID (for notifications)

Install & build

pnpm install
pnpm build

Usage

node dist/index.js \
  --n <count> \
  --bzz <amount> \
  --postage-batch-id <batchId1>,<batchId2> \
  --external-wallet <address> \
  --private-keys-path <path> \
  --json-rpc-url <rpcUrl> \
  --telegram-token <token> \
  --telegram-chat-id <chatId>

All arguments are required.

Arguments

| Argument | Description | Example | |---|---|---| | --n | Number of Bee nodes to manage | 3 | | --bzz | BZZ amount per operation | 0.5 | | --postage-batch-id | Comma-separated postage batch IDs to top up | abc...,def... | | --external-wallet | Ethereum address for fund transfers | 0x123... | | --private-keys-path | Path to file with private keys (one per line) | /etc/staker/keys.txt | | --json-rpc-url | Gnosis Chain JSON-RPC endpoint | https://rpc.gnosischain.com | | --telegram-token | Telegram Bot API token | 123456:ABC... | | --telegram-chat-id | Telegram chat ID for notifications | 123456789 |

Cron

The tool does not schedule itself; the invocation interval is whatever the cronjob uses. For example, every 5 minutes:

*/5 * * * * /usr/bin/node /opt/staker/dist/index.js --n 3 --bzz 0.5 ... >> /var/log/staker.log 2>&1

The process exits 0 after a completed (or intentionally skipped) tick, and 1 on a validation failure or a failed action.

Private keys file

Plain text, one private key per line, with or without 0x prefix. Must contain exactly n keys. On startup the tool validates each key against the corresponding node's Ethereum address and exits if any mismatch is detected.

Project structure

src/index.ts      — all application logic (single file)
dist/index.js     — compiled output (generated by build)
package.json      — dependencies and build script
tsconfig.json     — TypeScript config (ES2022, strict)

Key constants (hardcoded in src/index.ts)

| Name | Value | Purpose | |---|---|---| | BASE_PORT | 1633 | Starting port for Bee node discovery | | BZZ_ADDRESS | 0xdbf3ea6f5bee45c02255b2c26a16f300502f68da | BZZ token contract on Gnosis Chain | | WXDAI_ADDRESS | 0xe91d153e0b41518a2ce8dd3d7944fa863463a97d | WXDAI token contract; the router's WETH9 | | SUSHI_V3_FACTORY | 0xf78031cbca409f2fb6876bdfdbc1b2df24cf9bef | SushiSwap V3 factory, used to look up the BZZ/WXDAI pool | | SUSHI_V3_ROUTER | 0x4f54dd2f4f30347d841b7783ad08c050d8410a9d | SushiSwap V3 SwapRouter (Uniswap V3 periphery) | | SUSHI_POOL_FEE | 3000 | Fee tier of the BZZ/WXDAI pool, in hundredths of a bip (0.3%) | | SWAP_SLIPPAGE_PERCENT | 5 | How far below the spot quote amountOutMinimum is set | | MIN_XDAI_BALANCE | 0.1 xDAI | Below this a picked node swaps BZZ for xDAI instead of acting | | PLUR_DIGITS | 16 | Decimal scale of BZZ amounts (1 BZZ = 10^16 PLUR) | | XDAI_DIGITS | 18 | Decimal scale of xDAI/WXDAI amounts |

Dependencies

  • axios — HTTP client for every outgoing request (Bee nodes, JSON-RPC, Telegram)
  • viem — Ethereum client for signing and broadcasting transactions on Gnosis Chain
  • cafe-utility — CLI argument parsing, response validation (Types), and FixedPointNumber for BZZ amounts

There is no @ethersphere/bee-js dependency: the Bee HTTP API is called directly (see below), and BZZ amounts are plain FixedPointNumbers.

For agents

  • Entry point: src/index.ts — the entire application is one file; start there for any changes
  • Build command: pnpm build (runs tsc)
  • No tests exist — verify changes by reading the logic and checking TypeScript compilation
  • No environment variables — all configuration is via CLI arguments
  • Blockchain network: Gnosis Chain (chain ID 100); do not change the target network without updating BZZ_ADDRESS and the viem chain config
  • One process = one tick: main() validates keys, calls runTick() once, and returns. There is no loop and no scheduling — cron owns the interval. Do not reintroduce System.forever() or a --sleep argument.
  • Action selection is uniformly random per tick over ACTIONS (topup, stake, aid, transfer); there is no priority order and no fallback. An action returning false (e.g. tryAid() when n === 1) simply means the tick did nothing.
  • The swap is the one exception to random selection: runTick() checks the picked node's xDAI balance against MIN_XDAI_BALANCE first and, if it is short, runs doSwap() and returns without rolling for an action. Keep it that way — it is what makes nodes self-sustaining. Do not add it to ACTIONS.
  • Swap encoding: the router is Uniswap V3 periphery SwapRouter, not SwapRouter02exactInputSingle takes a deadline inside its params struct and only multicall(bytes[]) exists (no multicall(uint256, bytes[])). exactInputSingle has no address(0) recipient sentinel either, so the intermediate recipient is spelled out as SUSHI_V3_ROUTER for unwrapWETH9 to pick up.
  • unwrapWETH9 forwards the router's whole WXDAI balance to the recipient and reverts (STE) if the recipient cannot accept native xDAI. That is fine for node wallets, which are EOAs.
  • Gas limits are explicit on every transaction, which also stops viem from calling eth_estimateGas. Measured against live Gnosis state: approve uses ~52k (limit 100k) and the swap multicall ~171k (limit 400k).
  • Contract reads (allowance, getPool, slot0, token0) go through ethCall(), which reuses fetchJsonRpcHexString(); encoding and decoding are viem's encodeFunctionData/decodeFunctionResult. The module-level publicClient exists only to await receipts — do not route ordinary reads through it.
  • Never set nonce by hand. The Bee node signs with the same key as this tool (redistribution claims, cheque cashouts, its own stake and batch transactions), so the nonce sequence is shared and not predictable from here. Omitting nonce makes viem read the pending count immediately before signing — the latest possible moment — and a Bee transaction that is already mined or queued just moves us to the next slot. Precomputing nonce + 1 for a follow-up transaction is specifically wrong: the gap between the two sends is long enough for Bee to claim that slot, and the tool would then replace Bee's transaction or be replaced by it.
  • Every transaction sent by viem is confirmed with confirmTransaction() before it is reported as a success, so "succeeded" means mined, not merely accepted by the RPC. It checks three things in order: that receipt.transactionHash still matches the hash sent (viem resolves with the replacement's receipt when something else takes our nonce, rather than throwing, so without this check a Bee transaction winning the slot would read as our success), that receipt.status is not reverted, and otherwise logs the block. findRevertReason() then replays a reverted call to recover its reason, best effort. Transactions the Bee API sends (stake, top-up) need none of this — Bee waits for them itself.
  • Stake is doStake() — an unconditional deposit; aid is tryAid() — a BZZ transfer to a random other node. Neither consults current stake levels.
  • ERC-20 calls use viem's exported erc20Abi (transfer, approve, allowance); the previously hand-rolled transfer ABI is gone. Because erc20Abi is strongly typed, addresses must be 0x-prefixed — pass CLI-supplied addresses through ensure0x().
  • Top-up is unconditionaltryTopup() picks a random ID from --postage-batch-id and tops it up. There is no TTL guard; the https://bzz.limo/batches lookup that used to filter for TTL < 1 year is gone. Do not reintroduce it. The only remote read left is getGlobalPostageBatches(), needed for the batch depth in the amount formula.
  • Telegram reporting wraps every action via runAction(); if adding a new action, follow the same pattern. Failures go through describeError(), which prefers viem's shortMessage — stringifying a viem error yields a ~28 line, up to ~3000 character dump that buries the reason, while shortMessage keeps the revert reason (Execution reverted with reason: Too little received.) on one line. sendTelegramMessage() also caps text at TELEGRAM_MAX_LENGTH, because Telegram answers an overlong message with an HTTP 400 that is only logged, so the report would be lost. The full error still reaches the log, since runAction() rethrows.
  • Idle ticks report tooreportNoEligibleNodes() sends the fleet's total BZZ and its highest single-node balance (with that node's port, marked when frozen) whenever nothing is eligible, so silence on Telegram means the tick did not run rather than that it found nothing. Frozen nodes count towards both figures: a freeze lifts on its own, so the balance is the number worth watching. scanNodes() returns every node's balances and freeze state and runTick() filters it, which is what makes those figures available without a second round of Bee calls.
  • Node indexing: nodes are indexed 0 to n-1; port for node i is BASE_PORT + i; private key index matches node index
  • Never use fetch — Node's fetch refuses to connect to ports on the WHATWG "bad port" blocklist (1719, 1720, 2049, 6000, …), which node BASE_PORT + i can land on. All requests go through axios, whose Node adapter has no such restriction. Bee endpoints are called through beeRequest(); do not reintroduce bee-js (or fetch) for them.
  • BZZ amounts are FixedPointNumbers at scale PLUR_DIGITS; .value is the amount in PLUR (what the Bee API and the token contract take), .toDecimalString() is the human-readable BZZ form, and comparisons go through .compare(). This is exactly what bee-js's BZZ class wrapped. xDAI amounts are the same type at XDAI_DIGITS; do not compare the two scales against each other.
  • getWallet() returns both balances from one GET /wallet call (bzzBalance and nativeTokenBalance), so no extra RPC round trip is needed to check gas.