Los percentiles de recompensa de eth_feeHistory ofrecen una vista por bloque de la comisión prioritaria efectiva pagada por las transacciones en percentiles elegidos del conjunto de transacciones ordenadas por comisión del bloque. A diferencia de la comisión base, que está fijada por la regla del protocolo EIP-1559 a partir del uso de gas del bloque anterior, la comisión prioritaria es una estimación y el percentil es una entrada de control. Un estimador autocorrectivo envía transacciones, observa la latencia de inclusión y mueve el percentil hacia arriba o hacia abajo contra un objetivo con histéresis. Este artículo proporciona código Node.js ejecutable para el estimador y el bucle de retroalimentación, una tabla de resultados para completar con tu propio endpoint y solución de problemas para límites del proveedor, arreglos de recompensa en cero y bloques no finalizados.
El contrato de solicitud para eth_feeHistory
La especificación de Ethereum JSON-RPC define eth_feeHistory con tres parámetros: blockCount (el número de bloques a consultar), newestBlock (el número de bloque más alto o una etiqueta como 'latest') y rewardPercentiles (un arreglo de valores de percentil entre 0 y 100). El arreglo rewardPercentiles debe ser monótonamente creciente; el arreglo reward de la respuesta se alinea con estos percentiles en el mismo orden. Este es el contrato contra el que construyes, y está documentado en la especificación de Ethereum JSON-RPC.
La respuesta contiene cuatro arreglos. oldestBlock es el número del primer bloque de la ventana. baseFeePerGas tiene longitud blockCount + 1 porque incluye la comisión base del bloque siguiente al último bloque consultado, que es la comisión base del próximo bloque. gasUsedRatio tiene longitud blockCount y da la fracción de gas utilizado en cada bloque. reward es un arreglo de arreglos: para cada bloque, un arreglo de comisiones prioritarias efectivas pagadas por las transacciones en los percentiles solicitados, en wei. Una trampa común es la conversión de unidades: todos los valores están en wei, y convertirlos a gwei requiere dividir entre 1e9. Si pasas valores en wei a un campo denominado en gwei, tu transacción tendrá un precio inferior por un factor de mil millones.
El arreglo reward es la única parte de la respuesta que refleja lo que los usuarios pagaron realmente. No es una predicción; es una observación histórica de las comisiones prioritarias efectivas en los percentiles elegidos. El percentil que elijas determina qué transacción del conjunto ordenado por comisión de cada bloque estás muestreando. Un percentil bajo muestrea transacciones baratas que pueden haber esperado muchos bloques; un percentil alto muestrea transacciones que pagaron más para incluirse rápidamente.
- blockCount: número de bloques a consultar; los proveedores pueden limitar este valor.
- newestBlock: número de bloque más alto o 'latest'; debe ser un bloque que exista en la cadena canónica.
- rewardPercentiles: arreglo monótonamente creciente de números entre 0 y 100.
- oldestBlock: primer número de bloque de la ventana devuelta.
- baseFeePerGas: arreglo de longitud blockCount + 1; el último elemento es la comisión base del próximo bloque.
- gasUsedRatio: arreglo de longitud blockCount; fracción de gas utilizado por bloque.
- reward: arreglo de longitud blockCount; cada elemento es un arreglo de comisiones prioritarias efectivas alineadas con rewardPercentiles.
La comisión base está fijada por el protocolo, no es un problema de predicción
EIP-1559 define la regla de actualización de la comisión base: la comisión base del próximo bloque es una función del uso de gas del bloque anterior en relación con el objetivo. Si el bloque anterior usó exactamente el objetivo, la comisión base se mantiene igual. Si usó más, la comisión base aumenta; si usó menos, disminuye. El cambio está acotado por un factor máximo por bloque. Esta regla es determinista y está documentada en EIP-1559.
Debido a que la comisión base está fijada por el protocolo, tu estimador debe calcular la próxima comisión base a partir de la regla en lugar de extrapolar o promediar comisiones base históricas. El promedio introduce retraso y puede subestimar durante períodos de comisión base en aumento. El análisis independiente en Base Fee Manipulation In Ethereum's EIP-1559 Transaction Fee Mechanism muestra que el comportamiento de la comisión base está impulsado por la política y puede ser influenciado por mineros o validadores a lo largo de los bloques, lo que refuerza que no es un problema de predicción de demanda. Usa la regla del protocolo, no un modelo estadístico.
La implicación práctica es que maxFeePerGas debe establecerse como nextBaseFee * safetyMultiplier + maxPriorityFeePerGas. El multiplicador de seguridad tiene en cuenta la posibilidad de que la comisión base aumente antes de que se incluya tu transacción. Una elección común es 1.125 o 1.25, pero el valor correcto depende de cuántos bloques estés dispuesto a esperar y de cuán volátil sea la comisión base en tu cadena.
El percentil de la comisión prioritaria como entrada de control
La comisión prioritaria es la parte de la comisión que va al productor del bloque. A diferencia de la comisión base, no está fijada por el protocolo; es un resultado del mercado. El parámetro rewardPercentiles te permite muestrear la distribución de comisiones prioritarias efectivas dentro de cada bloque. Por ejemplo, rewardPercentiles: [10, 50, 90] devuelve, para cada bloque, la comisión prioritaria efectiva pagada por la transacción en el percentil 10, 50 y 90 de las transacciones ordenadas por comisión de ese bloque.
Un percentil demasiado bajo produce una transacción que no se incluye, porque otras transacciones ofrecen más. Un percentil demasiado alto paga de más, porque estás pagando más de lo necesario para incluirte. El percentil correcto no es un número fijo; depende de las condiciones actuales de la red, del valor que le das a la latencia de inclusión y del comportamiento de otras transacciones en los bloques que estás muestreando.
Esta es la parte que la mayoría de la documentación de proveedores no explica: el percentil es una entrada de control cuyo valor correcto depende de los resultados de inclusión observados. No puedes copiar un percentil de una publicación de blog y esperar que funcione en todas las cadenas y condiciones de mercado. Debes medir tu propia latencia de inclusión y ajustar.
El bucle cerrado: enviar, observar, ajustar
Un estimador autocorrectivo trata el percentil como una variable en un bucle de retroalimentación. El bucle tiene cuatro etapas: estimar, enviar, observar, ajustar. En la etapa de estimación, llamas a eth_feeHistory con tu conjunto actual de percentiles y calculas maxFeePerGas y maxPriorityFeePerGas. En la etapa de envío, firmas y envías la transacción. En la etapa de observación, registras el número de bloque al enviar y el número de bloque al incluirse, y calculas la latencia de inclusión en bloques. En la etapa de ajuste, comparas la latencia observada con un objetivo y mueves el percentil hacia arriba o hacia abajo.
La histéresis es importante para evitar la oscilación. Si ajustas el percentil en cada transacción, puedes pasarte y crear un ciclo de pagar de más y pagar de menos. Un enfoque simple es ajustar solo cuando la latencia observada se desvía del objetivo en más de un umbral, o ajustar en un paso pequeño y requerir múltiples observaciones antes de un cambio mayor. Por ejemplo, si el objetivo es dos bloques y la latencia observada es de cinco bloques, aumenta el percentil en 5 puntos. Si la latencia observada es de un bloque, disminúyelo en 2 puntos. Si la latencia observada es de dos o tres bloques, no hagas nada.
Este bucle cerrado es la parte que ningún resultado de la primera página cubre. La documentación del proveedor describe la solicitud y la respuesta, pero no cómo usar la respuesta para impulsar una decisión de control. El bucle es lo que convierte a eth_feeHistory de una fuente de datos en un estimador autocorrectivo.
- Estimar: llama a eth_feeHistory con el conjunto actual de percentiles.
- Enviar: firma y envía la transacción con las comisiones calculadas.
- Observar: registra el bloque de envío y el bloque de inclusión; calcula la latencia en bloques.
- Ajustar: compara la latencia con el objetivo; mueve el percentil hacia arriba o hacia abajo con histéresis.
Estimador ejecutable en Node.js usando eth_feeHistory
El siguiente script de Node.js llama a eth_feeHistory con un conjunto de percentiles configurable, calcula la próxima comisión base a partir de la regla EIP-1559, toma la comisión prioritaria en un percentil elegido a lo largo de la ventana (usando la mediana de los valores por bloque, no un solo bloque), añade un multiplicador de seguridad y emite maxFeePerGas y maxPriorityFeePerGas. Utiliza la API fetch integrada y asume una URL de endpoint JSON-RPC en la variable de entorno RPC_URL.
El script calcula la próxima comisión base aplicando la regla EIP-1559 al último bloque de la ventana. Utiliza el gasUsedRatio del último bloque y el baseFeePerGas del último bloque para calcular la próxima comisión base. Luego toma la mediana de los valores de recompensa en el índice de percentil elegido en todos los bloques de la ventana. Esta mediana es más robusta que el valor de un solo bloque, que puede ser un valor atípico.
El multiplicador de seguridad se aplica solo al componente de la comisión base. La comisión prioritaria se suma después del multiplicador. Esto coincide con la semántica de EIP-1559: maxFeePerGas es la comisión total máxima por gas, y maxPriorityFeePerGas es la comisión prioritaria máxima por gas. La transacción es válida solo si maxFeePerGas >= baseFeePerGas + maxPriorityFeePerGas en el momento de la inclusión.
const RPC_URL = process.env.RPC_URL;
const PERCENTILES = [10, 25, 50, 75, 90];
const CHOSEN_PERCENTILE_INDEX = 2; // 50th percentile
const BLOCK_COUNT = 20;
const SAFETY_MULTIPLIER = 1.125;
async function rpc(method, params) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(JSON.stringify(json.error));
return json.result;
}
function median(values) {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}
function nextBaseFee(lastBaseFee, lastGasUsedRatio) {
const target = 0.5;
const maxChange = 0.125;
const delta = (lastGasUsedRatio - target) / target;
const bounded = Math.max(-maxChange, Math.min(maxChange, delta));
return Math.floor(lastBaseFee * (1 + bounded));
}
async function estimateFees() {
const history = await rpc('eth_feeHistory', [
'0x' + BLOCK_COUNT.toString(16),
'latest',
PERCENTILES
]);
const baseFees = history.baseFeePerGas.map((hex) => parseInt(hex, 16));
const gasRatios = history.gasUsedRatio;
const rewards = history.reward.map((blockRewards) =>
blockRewards.map((hex) => parseInt(hex, 16))
);
const lastBaseFee = baseFees[baseFees.length - 2];
const lastGasRatio = gasRatios[gasRatios.length - 1];
const nextBase = nextBaseFee(lastBaseFee, lastGasRatio);
const prioritySamples = rewards.map((block) => block[CHOSEN_PERCENTILE_INDEX]);
const medianPriority = median(prioritySamples);
const maxPriorityFeePerGas = Math.ceil(medianPriority);
const maxFeePerGas = Math.ceil(nextBase * SAFETY_MULTIPLIER) + maxPriorityFeePerGas;
return {
nextBaseFee: nextBase,
medianPriority,
maxPriorityFeePerGas,
maxFeePerGas,
maxPriorityFeePerGasGwei: maxPriorityFeePerGas / 1e9,
maxFeePerGasGwei: maxFeePerGas / 1e9
};
}
estimateFees().then(console.log).catch(console.error);Medición de la tasa de inclusión y actualización del percentil
El segundo fragmento ejecutable mide la tasa de inclusión a partir de los datos de inclusión observados y actualiza el percentil. Asume que tienes una lista de transacciones recientes con su bloque de envío, bloque de inclusión y el percentil utilizado. Calcula la latencia de inclusión promedio y ajusta el percentil hacia arriba o hacia abajo con histéresis. La lógica de ajuste es intencionalmente simple: si la latencia promedio supera el objetivo en más de un bloque, aumenta el percentil en un paso; si está por debajo del objetivo en más de un bloque, disminúyelo en un paso más pequeño; de lo contrario, déjalo sin cambios.
El fragmento también calcula una métrica de sobrepago: la diferencia entre la comisión prioritaria que pagaste y la comisión prioritaria mediana en el percentil elegido en el bloque de inclusión. Esto te ayuda a distinguir entre 'incluida' e 'incluida barata'. Una transacción que se incluye en un bloque pero paga el percentil 90 cuando el percentil 50 habría sido suficiente es candidata a un percentil más bajo.
En producción, persistirías el percentil y el historial de observaciones en una base de datos o un archivo. El fragmento usa un objeto en memoria por claridad. El punto clave es que el percentil no es una constante; es una variable de estado que evoluciona con los resultados observados.
const TARGET_LATENCY_BLOCKS = 2;
const HYSTERESIS_BLOCKS = 1;
const UP_STEP = 5;
const DOWN_STEP = 2;
const MIN_PERCENTILE = 5;
const MAX_PERCENTILE = 95;
function updatePercentile(currentPercentile, observations) {
if (observations.length === 0) return currentPercentile;
const avgLatency =
observations.reduce((sum, o) => sum + (o.inclusionBlock - o.submissionBlock), 0) /
observations.length;
const deviation = avgLatency - TARGET_LATENCY_BLOCKS;
if (deviation > HYSTERESIS_BLOCKS) {
return Math.min(MAX_PERCENTILE, currentPercentile + UP_STEP);
}
if (deviation < -HYSTERESIS_BLOCKS) {
return Math.max(MIN_PERCENTILE, currentPercentile - DOWN_STEP);
}
return currentPercentile;
}
function overpayment(paidPriorityFee, medianPriorityFeeAtInclusion) {
return paidPriorityFee - medianPriorityFeeAtInclusion;
}
// Example usage
const observations = [
{ submissionBlock: 100, inclusionBlock: 105, percentile: 50, paidPriorityFee: 2e9 },
{ submissionBlock: 106, inclusionBlock: 108, percentile: 50, paidPriorityFee: 2e9 },
{ submissionBlock: 109, inclusionBlock: 110, percentile: 50, paidPriorityFee: 2e9 }
];
const currentPercentile = 50;
const newPercentile = updatePercentile(currentPercentile, observations);
console.log({ currentPercentile, newPercentile });
const over = overpayment(2e9, 1.5e9);
console.log({ overpaymentWei: over, overpaymentGwei: over / 1e9 });Tabla de resultados para tu propio endpoint
Usa la siguiente tabla para registrar mediciones con tu propio endpoint. Ejecuta el estimador con diferentes percentiles, envía transacciones y registra la recompensa mediana en el percentil elegido, los bloques hasta la inclusión y el sobrepago en relación con la recompensa mediana en la inclusión. El objetivo es encontrar el percentil más bajo que cumpla de forma consistente con tu latencia de inclusión objetivo.
Completa la tabla durante un período de al menos unas pocas horas para capturar la variación en las condiciones de la red. Una sola muestra no es suficiente para sacar conclusiones. Si estás probando en una cadena de baja actividad, es posible que debas esperar más o usar una testnet con tráfico controlado.
La columna de sobrepago es la diferencia entre la comisión prioritaria que pagaste y la comisión prioritaria mediana en el mismo percentil en el bloque de inclusión. Un valor positivo significa que pagaste más que la mediana; un valor negativo significa que pagaste menos. Un sobrepago consistentemente positivo sugiere que puedes bajar el percentil.
- Percentil: el valor de rewardPercentiles utilizado para la estimación.
- Recompensa mediana (gwei): la mediana de los valores de recompensa por bloque en ese percentil.
- Bloques hasta la inclusión: bloque de inclusión menos bloque de envío.
- Sobrepago (gwei): comisión prioritaria pagada menos comisión prioritaria mediana en la inclusión.
Modos de fallo y solución de problemas
Los límites de blockCount del proveedor son un modo de fallo común. Muchos proveedores limitan el número de bloques que puedes consultar en una sola llamada a eth_feeHistory. Cuando superas el límite, el proveedor devuelve un objeto de error JSON-RPC. La especificación JSON-RPC 2.0 define el envoltorio de error: un código, un mensaje y datos opcionales. Debes decodificar este error y manejarlo, no ignorarlo. Un estimador robusto captura el error, reduce blockCount y reintenta. Si ignoras el error, tu estimador puede usar datos obsoletos o faltantes.
Un arreglo de recompensa todo ceros puede ocurrir en cadenas de baja actividad o durante períodos en los que ninguna transacción pagó una comisión prioritaria. En este caso, la comisión prioritaria mediana es cero, y tu transacción puede incluirse con una comisión prioritaria cero si la comisión base es suficiente. Sin embargo, una comisión prioritaria cero puede no ser aceptada por todos los productores de bloques. Una alternativa es usar eth_maxPriorityFeePerGas, que devuelve una comisión prioritaria sugerida, o establecer un piso mínimo de comisión prioritaria.
Un newestBlock que aún no está finalizado puede hacer que tu estimador use un bloque que puede ser reorganizado. Si el bloque se reorganiza, los datos de comisión base y recompensa pueden cambiar. Para la mayoría de los casos de uso, usar 'latest' es aceptable, pero para transacciones de alto valor, es posible que quieras usar un bloque unos pocos por detrás de la cabeza para reducir el riesgo de reorganización. La contrapartida es que los datos están ligeramente obsoletos.
El respaldo de maxPriorityFeePerGas es útil cuando la ventana de historial no es representativa. Si la ventana contiene solo bloques de baja actividad, los percentiles de recompensa pueden ser cero o cercanos a cero. En ese caso, eth_maxPriorityFeePerGas proporciona un valor sugerido por el proveedor que puede ser más apropiado. El comportamiento documentado varía según el proveedor; algunos proveedores calculan este valor a partir de bloques recientes, mientras que otros usan una heurística fija.
- Límite de blockCount del proveedor: decodifica el objeto de error JSON-RPC y reintenta con un blockCount menor.
- Arreglo de recompensa todo ceros: usa eth_maxPriorityFeePerGas o establece un piso mínimo de comisión prioritaria.
- newestBlock no finalizado: usa un bloque unos pocos por detrás de la cabeza para transacciones de alto valor.
- Ventana de historial no representativa: recurre a eth_maxPriorityFeePerGas o amplía la ventana.
Limitaciones y contrapartidas
El tráfico de estimación de gas tiene un costo. Cada llamada a eth_feeHistory consume recursos del proveedor y puede contar contra tus límites de tasa. Un estimador autocorrectivo que llama a eth_feeHistory en cada transacción puede generar más tráfico que un estimador simple que almacena en caché el resultado durante unos pocos bloques. La contrapartida está entre la capacidad de respuesta y el costo. Para la mayoría de las aplicaciones, almacenar en caché el resultado durante uno o dos bloques es suficiente.
Un percentil del historial no puede predecir un pico. Si una transacción grande o un lote de transacciones entra en el mempool, la comisión prioritaria requerida para una inclusión oportuna puede subir bruscamente. Los percentiles históricos reflejan condiciones pasadas, no la demanda futura. El multiplicador de seguridad sobre la comisión base ayuda con los picos de comisión base, pero el componente de comisión prioritaria no está protegido. Para transacciones sensibles al tiempo, considera un percentil más alto o un multiplicador dinámico.
La diferencia entre 'incluida' e 'incluida barata' importa. Una transacción que se incluye en un bloque en el percentil 90 no es necesariamente mejor que una transacción que se incluye en dos bloques en el percentil 50. La primera paga de más; la segunda puede ser más rentable. Tu latencia objetivo debe reflejar el valor de la velocidad de inclusión para tu caso de uso. Para una transferencia no urgente, dos o tres bloques pueden ser aceptables; para una transacción de arbitraje, un bloque puede ser esencial.
El bucle cerrado requiere observación. Debes registrar los bloques de envío e inclusión de tus propias transacciones. Esto es sencillo si controlas la billetera de envío, pero es más difícil si estás estimando para un tercero. En ese caso, puedes usar un conjunto de datos público o una API de explorador de bloques para observar la latencia de inclusión de transacciones con parámetros de comisión similares.
Próximos pasos y guías relacionadas
Para profundizar en el método eth_feeHistory en sí, incluida la forma de la respuesta y un estimador simple, consulta Estimación del precio del gas con eth_feeHistory. Esa página cubre el método y la trampa del doble conteo de la comisión base; esta página cubre el bucle cerrado que ajusta el percentil.
Para manejar transacciones atascadas o con precio insuficiente, consulta Transacciones de reemplazo y con precio insuficiente con eth_sendRawTransaction. Para la gestión de nonces, que es esencial al enviar múltiples transacciones, consulta Gestión de nonces en EVM con eth_getTransactionCount. Para inspeccionar el mempool y entender qué están pagando otras transacciones, consulta El espacio de nombres txpool de Ethereum e inspección del mempool.
Para la selección de endpoints y comparación de proveedores, consulta la Lista de RPC de Ethereum y selección de endpoints (RPC Assistant). Para detalles específicos de la red, consulta la página de la red Ethereum. Para precios y límites de tasa, consulta Precios de RPC y el servicio de API. Para una visión más amplia de OnFinality Learn, consulta el centro de OnFinality Learn.
Si estás construyendo sobre una cadena OP-Stack, ten en cuenta que el componente de comisión de L1 es independiente de la comisión EIP-1559. Consulta Cálculo de la comisión L1 de OP-Stack y costo total de transacción para más detalles.