@raptortrade/priorityfee
v0.2.12
Published
TypeScript bindings for the Solana priority-fee operators on Raptor.
Readme
@raptortrade/priorityfee
TypeScript bindings for the Solana priority-fee operators on Raptor.
The package exposes typed Shapes and feature helpers for each emitted view. This README focuses on the recommendation views, which changed shape in 0.x - see Migration: 0.x congestion columns removed below.
Recommendation views
Two views, both produced by the priorityfee_recommendation / priorityfee_account_recommendation operators:
| RQL view | Helper | Row type |
|----------|--------|----------|
| solana::priorityfee::recommendation::rolling::{1s,10s,30s,1min,5min,1hr} | recommendation(duration) | RecommendationRow |
| solana::priorityfee::account::recommendation::rolling::{1s,10s,30s,1min,5min,1hr} | accountRecommendation(account, duration) | AccountRecommendationRow |
Each row contains six fee tiers (min, low, medium, high, very_high, max), two landing-probability views per tier ({tier}_landing_rank_pct and {tier}_landing_inclusion_pct), and a small set of context fields. Full schema in src/schema.ts / src/types.ts.
Block-fill metrics
The recommendation row now exposes two continuous block-fill metrics derived from the most recent ~50 blocks the operator has observed:
interface RecommendationRow {
// ... tier columns elided ...
/** Median per-block `100 * cu_consumed / cu_budgeted` across the recent block ring. */
median_block_fill_pct: Float8Value;
/** Share (%) of blocks in the ring at or above the saturation threshold (0.75). */
saturated_block_share_pct: Float8Value;
fee_volatility: Float8Value;
avg_cu_per_tx: Uint8Value;
}What each metric tells you
median_block_fill_pctcaptures the typical block: a high value means most recent blocks are nearly full, so paying a priority fee is increasingly necessary to outbid the field.saturated_block_share_pctcaptures spikiness: a low median with a high saturated share means the leader pipeline has hot blocks competing with cold blocks, which is precisely when priority bidding pays off.
Both are computed over the same 50-block ring used by {tier}_landing_inclusion_pct. The saturation cutoffs differ between the two metrics, on purpose:
{tier}_landing_inclusion_pctuses a0.90slack gate, answering "did the block have so much room that any tx clears regardless of fee".saturated_block_share_pctuses a0.75cutoff, answering "was the block actually full of real work". The lower value compensates for users systematically over-requestingcompute_unit_limit: a tx that consumes 80k CU often requests 200-300k, so the per-blockcu_consumed / cu_budgetedratio runs in the 30-70% range even on busy networks. A0.90cutoff for the saturation metric would never trip in practice;0.75catches the bias without being trigger-happy.
Reading the two side by side: landing_inclusion_pct is the inclusion model conditioned on a specific tier price; the block-fill metrics describe the underlying block conditions that drive that model.
Picking thresholds
The metrics are continuous; no level is implied. Bucket on the consumer side however suits your policy. Examples:
// Trader-style: only bump fees when blocks are visibly tight.
const shouldBidUp = median_block_fill_pct >= 70;
// Latency-sensitive: bump even if blocks aren't full on average,
// because spiky periods leak into low-fee landings.
const shouldBidUp = saturated_block_share_pct >= 30;
// Belt-and-suspenders: only bid up when both signals agree.
const shouldBidUp = median_block_fill_pct >= 70 && saturated_block_share_pct >= 30;If you need a trend signal (the old congestion_trend was -1/0/+1 deltas of the discrete level), compute the delta from successive emissions yourself. With continuous metrics the comparison is more meaningful: a 5%-point drop in median_block_fill_pct is a real change; a discrete level dropping from 3 to 2 might mean anything between a 1% and a 30% shift.
Per-account caveat
saturated_block_share_pct on AccountRecommendationRow is conditional on the account being active in the block. Quiet blocks where the account submitted nothing are not observed. For sparse accounts the metric is noisy; for active accounts it surfaces account-specific contention.
Migration: 0.x congestion columns removed
Previous versions emitted two columns that have been removed:
| Removed | Type | Replacement |
|---------|------|-------------|
| congestion_level | Uint1Value (0-4) | median_block_fill_pct: Float8Value (0-100) and/or saturated_block_share_pct: Float8Value (0-100) |
| congestion_trend | Int1Value (-1/0/+1) | Compute from successive emissions if needed |
Why the change
The old congestion_level was computed as max(rate_level, util_level) where:
rate_levelthresholded transactions/sec against fixed buckets [200, 400, 700, 1000]. Solana blocks are CU-bounded, not tx-count-bounded, so this signal isn't grounded in any property of the runtime - 200 lightweight votes and 200 heavy DeFi swaps would score the same.util_levelthresholdedtotal_cu_consumed / total_cu_budgetedsummed across the transactions in the window. That measures how well-tuned users' CU limits are, not how full blocks were. A window where every tx over-budgets by 2x but blocks are 30% full would reportutil = 50%and trip the level - opposite of reality.
The discrete max(...) was also lossy: a congestion_level=3 could come from either driver, and the two imply different fee strategies (broad fee competition vs. spiky leader pressure).
The new metrics use per-slot summaries from the operator's existing block ring - the same primitive landing_inclusion_pct already uses - and stay continuous so consumers can threshold them however they want.
Migration recipes
// If you displayed a 5-bucket bar:
const oldLevel = (row: { congestion_level: number }) => row.congestion_level;
// Approximation from the new metrics:
const newLevel = (row: RecommendationRow) => {
const fill = Number(row.median_block_fill_pct);
if (fill < 30) return 0; // Quiet
if (fill < 55) return 1; // Light
if (fill < 75) return 2; // Moderate
if (fill < 90) return 3; // Busy
return 4; // Saturated
};
// Trend (was congestion_trend):
let prev: number | null = null;
function trend(row: RecommendationRow): -1 | 0 | 1 {
const cur = Number(row.median_block_fill_pct);
if (prev == null) { prev = cur; return 0; }
const sign = cur > prev + 1 ? 1 : cur < prev - 1 ? -1 : 0;
prev = cur;
return sign;
}The 1-percentage-point hysteresis above is illustrative; tune it against the cadence of your view (1s updates jitter, 1hr updates barely move).
Stats views still emit congestion_level
StatsRow (from priorityfee_stats / priorityfee_account_stats, queried via stats(...)) keeps congestion_level for now - the stats operators are a different code path and weren't part of this change. If you consume both views in the same code path, treat them as separate types.
