diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..523dc9e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## 0.1.0 - 2026-10-11 + +First release. + +### Added +- Recommendation from the priority fees paid by non-vote transactions in the 3 most recent blocks: low, medium and high (50th, 75th and 90th percentile), as lamports per transaction and micro-lamports per compute unit. +- `--units` to choose the compute unit limit used for the prices. +- `--account` (repeatable, up to 128): per-account fee floor from `getRecentPrioritizationFees`, with base58 address validation. +- `--watch` and `--interval` to keep refreshing; failed refreshes are retried and watch mode stops after 5 in a row. +- `--json` output, pretty for a single run and one compact line per refresh with `--watch`. +- `--rpc` and the `SOL_PRIORITY_FEE_RPC` environment variable; only the RPC hostname is ever printed. +- Retry with backoff on HTTP 429 and plain-English RPC errors. +- Programmatic API exported from the package entry point. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..49b3cb0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +Thanks for helping. This project stays small on purpose: zero runtime dependencies, plain CommonJS, Node.js 18 or newer. + +## Setup + +```bash +git clone https://github.com/switch-afk/sol-priority-fee.git +cd sol-priority-fee +npm test +``` + +There is nothing to install. Tests use Node's built-in test runner. + +## Making a change + +1. Branch from `main`. +2. Add or update tests with your change. Tests must run offline: use the mock RPC in `tests/helpers/mock-rpc.js`, never a real endpoint. +3. Run `npm test` and make sure it passes. +4. Open a pull request that explains what changed and why. + +## Guidelines + +- No runtime dependencies. +- Never print a full RPC URL: it can contain an API key. Use `hostLabel` from `src/rpc.js`. +- Keep output plain and honest about what a number means. If a figure is a floor or an estimate, say so. +- Errors should be plain English with a hint on what to do next. + +## Reporting a problem + +Open an issue with the command you ran, the output, and your Node.js version. Remove any API keys from what you paste. \ No newline at end of file diff --git a/README.md b/README.md index fbc4da6..0b2e161 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,170 @@ # sol-priority-fee -A zero-dependency Node.js CLI that recommends a Solana priority fee based on recent network data. +[![Tests](https://github.com/switch-afk/sol-priority-fee/actions/workflows/test.yml/badge.svg)](https://github.com/switch-afk/sol-priority-fee/actions/workflows/test.yml) +[![npm](https://img.shields.io/npm/v/sol-priority-fee)](https://www.npmjs.com/package/sol-priority-fee) -> Status: early development. The scaffold is in place; fee fetching is coming in the next releases. - -## Planned usage +A zero-dependency Node.js CLI that recommends a Solana priority fee from what recent transactions actually paid, or from the fee floor of the accounts your transaction touches. ```bash npx sol-priority-fee -npx sol-priority-fee --account
-npx sol-priority-fee --watch -npx sol-priority-fee --json ``` -## Requirements +## Why not just call `getRecentPrioritizationFees`? + +When this tool was built, that RPC method, called without accounts, returned a fee of 0 for all 150 of the most recent slots on mainnet. Percentiles of zeros tell you nothing, so the default mode reads what non-vote transactions really paid in recent blocks instead. The per-account version of the same RPC method is still useful, and it is available with `--account`. + +## Usage + +```bash +# Recommendation from what transactions paid in the last 3 blocks +sol-priority-fee + +# Prices for a 60,000 compute unit limit +sol-priority-fee --units 60000 + +# Fee floor for landing a transaction that locks these accounts as writable +sol-priority-fee --account
[--account
...] + +# Keep refreshing every 10 seconds (Ctrl+C to stop) +sol-priority-fee --watch + +# Machine-readable output +sol-priority-fee --json +sol-priority-fee --watch --json --interval 5 +``` + +Requires Node.js 18 or newer (CI runs on 20 and 22). No dependencies. + +## Options -- Node.js 18 or newer +| Option | What it does | +| --- | --- | +| `--account
` | Fee floor for landing a transaction that locks this account as writable. Repeatable, up to 128. Addresses are validated as 32-byte base58 before any RPC call. | +| `--units ` | Compute unit limit used to convert between lamports and micro-lamports per compute unit. Default 200000, maximum 1400000. | +| `--watch` | Keep refreshing until you press Ctrl+C. A failed refresh is reported and retried; after 5 failures in a row it stops with exit code 1. | +| `--interval ` | Seconds to pause between refreshes with `--watch`. Default 10, from 5 to 3600. The time to fetch the data comes on top of this. | +| `--json` | Print JSON: one pretty object for a single run, one compact line per refresh with `--watch`. | +| `--rpc ` | RPC endpoint. Overrides the environment variable. | +| `-v`, `--version` | Print the version. | +| `-h`, `--help` | Show help. | ## RPC endpoint -The public mainnet RPC rate-limits heavily. Set your own endpoint with the `SOL_PRIORITY_FEE_RPC` environment variable or pass `--rpc `. +The endpoint comes from `--rpc`, then the `SOL_PRIORITY_FEE_RPC` environment variable, then the public mainnet RPC, which rate-limits heavily. Use your own endpoint for anything beyond a quick try. + +```bash +export SOL_PRIORITY_FEE_RPC="https://your-rpc-provider.example/your-key" +sol-priority-fee +``` + +Only the hostname of the endpoint is ever printed, because RPC URLs often contain API keys. + +Cost per refresh: +- Default mode: `getSlot` plus `getBlock` for 3 recent blocks, about 4 calls. Blocks are large, so on a free tier use a longer `--interval`. +- `--account` mode: 1 call (`getRecentPrioritizationFees`). + +## Output + +Default mode reads the priority fee each non-vote transaction paid in the 3 most recent blocks. `low`, `medium` and `high` are the 50th, 75th and 90th percentile. + +``` +Priority fee recommendation + + level lamports/tx micro-lamports/CU + low 8 40 + medium 607 3,035 + high 5,193 25,965 + +Per-CU price assumes a 200,000 compute unit limit (change it with --units). +Based on 1,052 non-vote transactions in 3 recent blocks (slots 455731533 to 455731535) via mainnet.helius-rpc.com. +56% of them paid a priority fee. +low, medium and high are the 50th, 75th and 90th percentile of the priority fee those transactions paid. +``` + +`--account` mode shows, per slot, the fee the RPC reports for landing a transaction that locks the account as writable, over the last 150 slots (about 60 seconds). This is a quiet account, so most of it is 0: + +``` +Priority fee floor for 1 account + + level lamports/tx micro-lamports/CU + low 0 0 + medium 0 0 + high 1 4 + +Lamports per transaction assume a 200,000 compute unit limit (change it with --units). +Based on 150 recent slots (455736543 to 455736692, about 60 seconds) via mainnet.helius-rpc.com. +For each slot the RPC reports the fee needed to land a transaction that locks this account as writable. +low, medium and high are the 50th, 75th and 90th percentile across those slots. +This is a floor for getting in, not a bid to win: if the account is contested, pay above it. +A low fee of 0 means at least half of the recent slots needed no priority fee for this account. +``` + +JSON (default mode): + +```json +{ + "mode": "recent-transactions", + "time": "2026-10-11T17:39:35.566Z", + "rpc": "mainnet.helius-rpc.com", + "computeUnits": 200000, + "slotRange": { "from": 455736632, "to": 455736634 }, + "blockCount": 3, + "sampleCount": 1498, + "payingCount": 1237, + "levels": { + "low": { "percentile": 50, "lamports": 809, "microLamportsPerUnit": 4045 }, + "medium": { "percentile": 75, "lamports": 4770, "microLamportsPerUnit": 23850 }, + "high": { "percentile": 90, "lamports": 10350, "microLamportsPerUnit": 51750 } + } +} +``` + +With `--account` the object has `"mode": "account-floor"`, an `accounts` array and a `zeroCount` instead of the block fields. + +## Using the numbers + +The value to set on a transaction is the micro-lamports per compute unit price, together with a compute unit limit. Use the same `--units` as the limit you set, so the lamports figure matches what you will pay. + +```js +const { ComputeBudgetProgram } = require('@solana/web3.js'); + +const instructions = [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 200000 }), + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 3035 }), + // ...your instructions +]; +``` + +Priority fee in lamports is `price in micro-lamports per CU * compute unit limit / 1,000,000`. + +## Programmatic use + +```js +const { resolveEndpoint, sampleRecentFees, summarizeFees } = require('sol-priority-fee'); + +(async () => { + const endpoint = resolveEndpoint(null); + const sample = await sampleRecentFees(endpoint, {}); + const summary = summarizeFees(sample, { computeUnits: 200000 }); + console.log(summary.levels.medium.microLamportsPerUnit); +})(); +``` ## Honest limits -A recommendation is a statistical guess from recent blocks, not a guarantee of inclusion. Fees can spike faster than any sample can show. +- A recommendation is a statistical guess from a small, recent sample, not a guarantee of inclusion. Fees can spike faster than any sample shows. +- Default mode samples 3 blocks. It excludes vote transactions and counts only the priority fee: the total fee minus 5,000 lamports per signature. Tips sent as separate transfers (for example to a bundle service) are not counted. +- The lamports-per-transaction figure in default mode is what transactions paid, whatever their compute unit limit was. The micro-lamports-per-CU figure converts it using `--units`, so it is only exact for transactions with that limit. +- `--account` mode covers about 60 seconds. Accounts go quiet quickly, so 0 is common for anything that is not contested right now. +- The per-account figure is a floor for getting in. If many transactions compete for the same account, pay above it. + +## Exit codes + +| Code | Meaning | +| --- | --- | +| 0 | Success, or stopped with Ctrl+C in watch mode | +| 1 | RPC error, no usable data, or watch mode gave up after 5 failed refreshes | +| 2 | Invalid options or addresses | ## Development @@ -31,6 +172,8 @@ A recommendation is a statistical guess from recent blocks, not a guarantee of i npm test ``` +Tests use Node's built-in test runner and a mock RPC helper, so they run offline. See [CONTRIBUTING.md](CONTRIBUTING.md). + ## License MIT \ No newline at end of file diff --git a/package.json b/package.json index c7a95af..5d02493 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,38 @@ { "name": "sol-priority-fee", "version": "0.1.0", - "description": "Recommended Solana priority fee from recent network data (zero-dependency CLI)", + "description": "Recommended Solana priority fee from what recent transactions paid (zero-dependency CLI)", "bin": { "sol-priority-fee": "bin/sol-priority-fee.js" }, "main": "src/index.js", "files": [ "bin", - "src" + "src", + "CHANGELOG.md" ], "engines": { "node": ">=18" }, "scripts": { - "test": "node --test" + "test": "node --test", + "prepublishOnly": "npm test" }, "keywords": [ "solana", "priority-fee", "compute-budget", - "cli" + "cli", + "rpc" ], + "repository": { + "type": "git", + "url": "git+https://github.com/switch-afk/sol-priority-fee.git" + }, + "bugs": { + "url": "https://github.com/switch-afk/sol-priority-fee/issues" + }, + "homepage": "https://github.com/switch-afk/sol-priority-fee#readme", "author": "Sybi", "license": "MIT" } \ No newline at end of file