@valve-tech/gas-oracle 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,216 @@
1
+ # AGENTS.md
2
+
3
+ Terse reference for AI agents (Claude Code, Cursor, Aider, etc.) integrating
4
+ `@valve-tech/gas-oracle`. The full README is for humans; this file is for
5
+ agents that need to ground their work in the package's actual surface
6
+ quickly.
7
+
8
+ ## What this package does
9
+
10
+ Multi-tier gas-fee oracle for EVM chains. Polls block + mempool data,
11
+ computes `slow` / `standard` / `fast` / `instant` priority-fee
12
+ recommendations from a gas-weighted percentile distribution, and serves
13
+ them via an in-memory cache. Pass it a viem `PublicClient` and call
14
+ `oracle.getState()` for sub-millisecond reads.
15
+
16
+ Zero runtime dependencies. `viem ^2.0.0` is the only peer dependency.
17
+
18
+ ## Public API
19
+
20
+ All exports live under `src/index.ts`. Default subpath is the canonical
21
+ oracle constructor; two named subpaths add viem integration.
22
+
23
+ ```ts
24
+ import {
25
+ createGasOracle, // primary constructor
26
+ normalizeMempool, // pure helper
27
+ findByHash, findByAddressNonce, findInMempool, // mempool lookups
28
+ tipForBlockPosition, // discriminated block-position query
29
+ // types
30
+ type GasOracle,
31
+ type CreateCreateGasOracleOptions,
32
+ type GasGasOracleState,
33
+ type TierRecommendation,
34
+ type RawTx,
35
+ type NormalizedMempool,
36
+ type MempoolHit,
37
+ type TxIdentifier,
38
+ type BlockPositionQuery,
39
+ type BlockPositionResult,
40
+ } from '@valve-tech/gas-oracle'
41
+ ```
42
+
43
+ ```ts
44
+ import { gasOracleActions } from '@valve-tech/gas-oracle/viem-actions'
45
+ // client.extend(gasOracleActions({ oracle }))
46
+ ```
47
+
48
+ ```ts
49
+ import { withGasOracle } from '@valve-tech/gas-oracle/viem-transport'
50
+ // withGasOracle(transport, { oracle, intercept: ['eth_gasFeeEstimate', ...] })
51
+ ```
52
+
53
+ ## Five types you must know
54
+
55
+ | Type | What it is |
56
+ |---|---|
57
+ | `CreateGasOracleOptions` | Constructor config. Required: `client`, `chainId`. Tuneables: `priorityFeeDecayCap`, `priorityModel`, `baseFeeLivenessBlocks`, `poll`, `keepMempoolSnapshot`. |
58
+ | `GasOracleState` | What `oracle.getState()` returns. Holds the four-tier `tiers` map plus latest `baseFee`, `bufferedBaseFee`, mempool snapshot if enabled. |
59
+ | `TierRecommendation` | Per-tier struct: `{ maxPriorityFeePerGas, maxFeePerGas, gasPrice, maxFeePerBlobGas }`. All `bigint`. |
60
+ | `RawTx` | One mempool entry: `{ hash, from, to, nonce, value, maxPriorityFeePerGas, maxFeePerGas, gas, ... }`. Nullable fields for fields the chain may not expose. |
61
+ | `BlockPositionQuery` | Discriminated union — five mutually-exclusive shapes (see below). |
62
+
63
+ ## The discriminated `BlockPositionQuery`
64
+
65
+ This is the API surface most likely to confuse — it's deliberately shaped
66
+ to make impossible queries unrepresentable. The `kind` field discriminates;
67
+ TypeScript will narrow the rest of the fields per branch.
68
+
69
+ ```ts
70
+ type BlockPositionQuery =
71
+ | { kind: 'rank'; rank: number } // "tip to land in top N"
72
+ | { kind: 'percentile'; percentile: number } // "tip to land at p_X"
73
+ | { kind: 'gasFromTop'; gas: bigint } // "tip to land within G gas of leader"
74
+ | { kind: 'aheadOf'; tx: TxIdentifier } // "tip to leapfrog this tx"
75
+ | { kind: 'behind'; tx: TxIdentifier } // "tip to slip in just behind this tx"
76
+ ```
77
+
78
+ `TxIdentifier` is `{ hash: string } | { address: string; nonce: number | bigint | string }`.
79
+ Both forms resolve against the latest mempool snapshot. The result is a
80
+ `BlockPositionResult`:
81
+
82
+ ```ts
83
+ {
84
+ requiredTip: bigint // min tip-per-gas to land at the position
85
+ pivot: TipSample | null // the boundary sample, or null if out of range
86
+ rank: number // approximate 0-indexed rank from top
87
+ gasFromTop: bigint // approximate gas-from-top
88
+ }
89
+ ```
90
+
91
+ ## Configuration patterns by chain class
92
+
93
+ | Chain class | `priorityModel` | `baseFeeLivenessBlocks` | Notes |
94
+ |---|---|---|---|
95
+ | Ethereum mainnet, Base, Arbitrum, OP | `'eip1559'` | 6 | Validators burn base fee; tip is the only revenue. |
96
+ | PulseChain (mainnet + testnet) | `'flat'` | 6 | Validators charge tips instead of burning. Type-2 vs legacy distinction is meaningless on flat-fee chains. |
97
+ | Anything where you're unsure | `'flat'` | 6 | More conservative — never under-counts spam. |
98
+
99
+ `priorityFeeDecayCap`: default is `WAD / 8` (12.5%/block) which matches
100
+ EIP-1559's natural decay. Tighten to `WAD / 20` for very low-volatility
101
+ chains; loosen to `null` for chains where you want the published tip to
102
+ free-fall during quiet windows.
103
+
104
+ ## Three integration shapes (pick one)
105
+
106
+ ### 1. Direct (no viem wrapper)
107
+
108
+ ```ts
109
+ const oracle = createGasOracle({ client, chainId: 1, priorityModel: 'eip1559' })
110
+ oracle.start()
111
+ const tier = oracle.getState()?.tiers.fast
112
+ ```
113
+
114
+ ### 2. viem-actions extension (recommended for app code)
115
+
116
+ ```ts
117
+ const client = createPublicClient({ chain: mainnet, transport: http() })
118
+ .extend(gasOracleActions({ chainId: 1, priorityModel: 'eip1559' }))
119
+
120
+ await client.getGasTiers() // GasOracleState
121
+ await client.getGasTier('fast') // TierRecommendation
122
+ await client.findTxInMempool({ hash: '0x...' }) // MempoolHit | null
123
+ await client.tipForBlockPosition({ kind: 'rank', rank: 50 }) // BlockPositionResult
124
+ client.stopGasOracle() // teardown
125
+ ```
126
+
127
+ `gasOracleActions(options)` accepts the same options as `createGasOracle`
128
+ (minus `client`, which it gets from the viem client at extension time)
129
+ plus `lifecycle: 'eager' | 'lazy'`. Default `'eager'` starts polling on
130
+ extension; `'lazy'` defers until the first read.
131
+
132
+ ### 3. viem-transport interception (drop-in for existing wagmi/viem code)
133
+
134
+ Default intercepts only `eth_gasFeeEstimate` (a Valve-specific multi-tier
135
+ RPC extension). Standard methods stay unintercepted unless opted in:
136
+
137
+ ```ts
138
+ const transport = withGasOracle(http(), {
139
+ chainId: 1,
140
+ priorityModel: 'eip1559',
141
+ intercept: {
142
+ eth_gasFeeEstimate: true, // default
143
+ eth_gasPrice: 'standard', // tier-required opt-in (no boolean default)
144
+ eth_maxPriorityFeePerGas: 'fast',
145
+ },
146
+ lifecycle: 'lazy', // optional: defer poll until first intercept
147
+ })
148
+
149
+ // Tearing down (e.g. test/HMR): cast back to GasOracleTransport
150
+ ;(transport as GasOracleTransport).stopGasOracle()
151
+ ```
152
+
153
+ `eth_gasPrice` and `eth_maxPriorityFeePerGas` reject a boolean for the
154
+ intercept config — a tier choice is required so that the returned number
155
+ doesn't silently depend on the package version's default.
156
+
157
+ ## Pitfalls (read these)
158
+
159
+ 1. **Don't poll `oracle.getState()` in a tight loop.** It's O(1) but you're
160
+ wasting CPU. Subscribe via `oracle.subscribe(callback)` for change
161
+ notifications and read state inside the callback.
162
+
163
+ 2. **One oracle per chain, ever.** Module-scope the oracle in your code so
164
+ it's started once. Construct + `start()` per request and you're paying
165
+ the warmup cost (~3-5 blocks of `eth_feeHistory` polling) every time.
166
+
167
+ 3. **`oracle.start()` returns immediately but tiers aren't populated until
168
+ the first poll completes.** `oracle.getState()` returns `null` during
169
+ warmup. Either handle the null case at the call site, or call
170
+ `await oracle.pollOnce()` once after `start()` to force a synchronous
171
+ first poll.
172
+
173
+ 4. **Mempool snapshot is opt-in.** `keepMempoolSnapshot: false` (the
174
+ default) means `findByHash`/`findInMempool`/`tipForBlockPosition` queries
175
+ that take a `TxIdentifier` will throw. Set `true` if you need them.
176
+
177
+ 5. **`findInMempool({ hash })` resolution costs a snapshot scan.** Cache
178
+ the result if you're calling it repeatedly for the same hash —
179
+ memoize or use a local map.
180
+
181
+ 6. **PulseChain RPCs may not honor `txpool_content`.** If
182
+ `oracle.getMempoolSnapshot()` returns `[]` consistently on a chain you
183
+ know has live txs, the upstream RPC is rejecting the namespace.
184
+ Verify with `curl -X POST <rpc> -H 'content-type: application/json' \
185
+ --data '{"jsonrpc":"2.0","method":"txpool_content","params":[],"id":1}'`.
186
+
187
+ ## Examples
188
+
189
+ Runnable scripts in `examples/`:
190
+
191
+ - `examples/01-basic-tiers.ts` — minimal `createGasOracle` + read tier
192
+ - `examples/02-mempool-snapshot.ts` — `keepMempoolSnapshot: true` + `findByHash`
193
+ - `examples/03-block-position.ts` — all five `tipForBlockPosition` query forms
194
+ - `examples/04-viem-actions.ts` — `client.extend(gasOracleActions(...))`
195
+ - `examples/05-viem-transport.ts` — `withGasOracle(transport, ...)` interception
196
+
197
+ Run any of them with `yarn tsx examples/01-basic-tiers.ts`.
198
+
199
+ ## Skills (for AI agents)
200
+
201
+ `skills/` directory ships in the npm tarball. If you're an AI agent working
202
+ in a project that has installed this package, look in
203
+ `node_modules/@valve-tech/gas-oracle/skills/SKILL.md` for trigger conditions
204
+ and integration recipes that go deeper than this file.
205
+
206
+ ## Verifying provenance
207
+
208
+ v0.2.3+ ships with SLSA provenance attestation:
209
+
210
+ ```bash
211
+ npm view @valve-tech/gas-oracle@latest --json | jq .dist.attestations
212
+ npm audit signatures
213
+ ```
214
+
215
+ The attestation links the published tarball to the GitHub Actions workflow
216
+ run that built it.
package/CHANGELOG.md ADDED
@@ -0,0 +1,113 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@valve-tech/gas-oracle` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.2.5] — 2026-05-03
9
+
10
+ ### Added
11
+ - README **RPC transport modes** section covering all four caller-side
12
+ configurations the package supports: HTTP-only, WS-only, both (via viem's
13
+ `fallback`), and "neither" (driving the pure `reducePollInputs` reducer
14
+ with pre-fetched `OraclePollInputs` — no live `PublicClient` needed).
15
+ - `examples/06-reducer-only.ts` exercising the offline path end-to-end with
16
+ synthetic fixture inputs, surfacing the `fetchOracleInputs` /
17
+ `reducePollInputs` export split that enables it.
18
+
19
+ ### Notes
20
+ - Documentation-only release. No API changes; behavior identical to v0.2.4.
21
+ - Picking WS today buys nothing functional over HTTP — the oracle never
22
+ opens a subscription. The functional case for WS arrives when
23
+ subscription-using features (e.g., tx-tracking via `newHeads` /
24
+ `newPendingTransactions`) land. Choose WS now only if upstream is cheaper
25
+ or lower-latency on it.
26
+
27
+ ## [0.2.4] — 2026-05-02
28
+
29
+ ### Added
30
+ - `CHANGELOG.md` (this file).
31
+ - `AGENTS.md` at repo root — terse, AI-first companion to README. Lists the
32
+ public API, the discriminated query shape for `tipForBlockPosition`, and
33
+ pitfalls.
34
+ - `examples/` directory with 5 runnable scripts covering basic tier reads,
35
+ mempool snapshots, block-position queries, and both viem subpaths.
36
+ - `skills/` directory shipped in the npm tarball — Claude Code / Cursor / etc.
37
+ agents consuming `node_modules/@valve-tech/gas-oracle/skills/` get grounded
38
+ context about when and how to use the package.
39
+ - README badges (npm version, types-included, SLSA provenance).
40
+ - ESLint configuration with `@typescript-eslint/no-explicit-any` enforced as
41
+ an error. The codebase was incidentally `any`-free; this makes the rule a
42
+ hard constraint.
43
+ - `lint` script wired into the CI workflow.
44
+
45
+ ### Changed
46
+ - `files` field in `package.json` now includes `CHANGELOG.md`, `AGENTS.md`,
47
+ and `skills/` so consumers get the docs and skill files in their
48
+ `node_modules/`.
49
+
50
+ ## [0.2.3] — 2026-05-02
51
+
52
+ ### Added
53
+ - First release published via npm trusted-publisher OIDC. SLSA provenance
54
+ attestation now ships with the tarball; consumers can verify with
55
+ `npm audit signatures`.
56
+
57
+ ### Fixed
58
+ - Aligned the release workflow with the known-working pattern used by other
59
+ OIDC-publishing repos: removed the `environment:` block from the publish
60
+ job, pinned `npm` to `11.5.1` before install. Without this, the OIDC PUT
61
+ to npm 404'd despite the trusted-publisher record being correctly
62
+ configured. See repo commit `de6c5bb` for the diagnostic.
63
+
64
+ ## [0.2.2] — *unpublished*
65
+
66
+ Tagged but never published. OIDC publish failed; abandoned in favor of
67
+ v0.2.3 which carries the workflow fix.
68
+
69
+ ## [0.2.1] — 2026-05-02
70
+
71
+ ### Fixed
72
+ - Top-level `main`, `types`, and `exports` now point at `dist/*` instead of
73
+ `src/*.ts`. The `publishConfig` override pattern that previously rewrote
74
+ these fields at publish time is deprecated in npm 11 and didn't apply
75
+ correctly during the v0.2.0 manual publish — that release shipped a
76
+ tarball whose `package.json` pointed at non-existent `src/` paths.
77
+ - Added `prepare: yarn build` to scripts so consumers using workspace
78
+ symlinks or `git+` installs get a built `dist/` automatically.
79
+
80
+ ### Removed
81
+ - The `publishConfig` block (deprecated; replaced by aligning top-level
82
+ fields with the published shape).
83
+
84
+ ## [0.2.0] — 2026-05-02
85
+
86
+ First public release. Tarball had a packaging bug — see [0.2.1] for the
87
+ fix. Consumers should install `@valve-tech/gas-oracle@^0.2.1` or later.
88
+
89
+ ### Added
90
+ - `priorityFeeDecayCap: bigint | null` config (wad; null = uncapped;
91
+ default `WAD/8` = 12.5%/block, EIP-1559 parity).
92
+ - `priorityModel: 'flat' | 'eip1559'` for chains whose validators charge
93
+ tips instead of burning them (`flat`) versus chains that honor
94
+ EIP-1559 ordering (`eip1559`).
95
+ - `baseFeeLivenessBlocks: number` — compounded 9/8 buffer over N blocks
96
+ so `maxFeePerGas` survives sustained worst-case base-fee growth.
97
+ - `poll: { feeHistory?, mempool? }` toggles for chains that don't expose
98
+ one or both endpoints.
99
+ - `keepMempoolSnapshot: boolean` + `oracle.getMempoolSnapshot()` for
100
+ Phase B stuck-tx detection.
101
+ - Pure helpers: `normalizeMempool`, `findByHash`, `findByAddressNonce`,
102
+ `findInMempool`.
103
+ - `tipForBlockPosition` — discriminated query over `rank` / `percentile`
104
+ / `gasFromTop` and `aheadOf` / `behind` a `TxIdentifier`.
105
+ - `@valve-tech/gas-oracle/viem-actions` subpath for
106
+ `client.extend(gasOracleActions(...))` integration.
107
+ - `@valve-tech/gas-oracle/viem-transport` subpath for `withGasOracle(transport, ...)`
108
+ drop-in interception.
109
+
110
+ [0.2.4]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.4
111
+ [0.2.3]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.3
112
+ [0.2.1]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.1
113
+ [0.2.0]: https://github.com/valve-tech/gas-oracle/releases/tag/v0.2.0
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @valve-tech/gas-oracle
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@valve-tech/gas-oracle)](https://www.npmjs.com/package/@valve-tech/gas-oracle)
4
+ [![Types Included](https://img.shields.io/npm/types/@valve-tech/gas-oracle)](https://www.npmjs.com/package/@valve-tech/gas-oracle)
5
+ [![SLSA Provenance](https://img.shields.io/badge/SLSA-provenance-blue)](https://docs.npmjs.com/generating-provenance-statements)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
3
8
  Multi-tier gas-fee oracle for EVM chains. Pass it a viem `PublicClient`
4
9
  and it polls block + mempool data, computes `slow` / `standard` /
5
10
  `fast` / `instant` tier recommendations, and serves them via an
@@ -8,6 +13,10 @@ aware EIP-1559 priority cutoff, and EIP-4844 blob-fee handling.
8
13
 
9
14
  Zero runtime dependencies. `viem` is the only peer dependency.
10
15
 
16
+ > **AI agents:** see [`AGENTS.md`](AGENTS.md) for a terse, AI-first reference,
17
+ > and [`skills/`](skills/) for Claude Code / Cursor skill files shipped in
18
+ > `node_modules/`.
19
+
11
20
  ## Install
12
21
 
13
22
  ```bash
@@ -258,6 +267,72 @@ careful to avoid.
258
267
  synthesizing a historical-percentile array from oracle state is its
259
268
  own design problem. Always passes through to upstream.
260
269
 
270
+ ## RPC transport modes
271
+
272
+ The package only ever calls `client.request({ method, params })` and
273
+ never opens a subscription. That makes it transport-agnostic — any
274
+ viem `Transport` works, and the four caller-side configurations below
275
+ all run unchanged:
276
+
277
+ ### HTTP only
278
+
279
+ ```ts
280
+ import { http } from 'viem'
281
+
282
+ const client = createPublicClient({ chain: mainnet, transport: http(rpcUrl) })
283
+ const oracle = createGasOracle({ client, chainId: 1 })
284
+ ```
285
+
286
+ ### WebSocket only
287
+
288
+ ```ts
289
+ import { webSocket } from 'viem'
290
+
291
+ const client = createPublicClient({ chain: mainnet, transport: webSocket(wsUrl) })
292
+ const oracle = createGasOracle({ client, chainId: 1 })
293
+ ```
294
+
295
+ WS works because the three RPCs the oracle issues (`eth_feeHistory`,
296
+ `eth_getBlockByNumber`, `txpool_content`) are all request/response;
297
+ viem's `webSocket` transport implements the same `request` interface
298
+ as `http`. **Picking WS today buys nothing functional over HTTP** —
299
+ the oracle still polls on its `pollIntervalMs`. The functional case
300
+ for WS arrives when subscription-using features land (tx-tracking
301
+ `newHeads` / `newPendingTransactions`); choose WS now only if your
302
+ upstream is cheaper or lower-latency on it.
303
+
304
+ ### Both — `fallback` for resilience
305
+
306
+ ```ts
307
+ import { fallback, http, webSocket } from 'viem'
308
+
309
+ const transport = fallback([webSocket(wsUrl), http(rpcUrl)])
310
+ const client = createPublicClient({ chain: mainnet, transport })
311
+ const oracle = createGasOracle({ client, chainId: 1 })
312
+ ```
313
+
314
+ viem handles failover transparently — if the WS drops, requests fall
315
+ to HTTP without the oracle noticing.
316
+
317
+ ### Neither — pure reducer, no live RPC
318
+
319
+ The oracle's I/O surface (`fetchOracleInputs`) and its math
320
+ (`reducePollInputs`) are exported as separate top-level entries. That
321
+ split is what enables the offline path: drive the reducer with
322
+ `OraclePollInputs` from any source — fixture file, snapshot store,
323
+ Kafka log, another service's API — and never touch a `PublicClient`.
324
+
325
+ ```ts
326
+ import { reducePollInputs, type OraclePollInputs } from '@valve-tech/gas-oracle'
327
+
328
+ const inputs: OraclePollInputs = await loadFromYourQueue()
329
+ const state = reducePollInputs({ inputs, chainId: 1, prev: priorState })
330
+ ```
331
+
332
+ Use cases: serverless / edge handlers, backtest harnesses replaying
333
+ historical RPC payloads, tests asserting state shape from fixtures.
334
+ See `examples/06-reducer-only.ts`.
335
+
261
336
  ## Wire format
262
337
 
263
338
  Every fee field is a `bigint`. Callers serializing across HTTP / Redis
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valve-tech/gas-oracle",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Multi-tier gas-fee oracle for EVM chains. Computes slow/standard/fast/instant tier recommendations from block-included tips, mempool pending tips, and base-fee trend, with a configurable downside-decay cap and a chain-aware EIP-1559 priority cutoff. viem-native — pass it a PublicClient and it does the rest. Ships viem-actions and viem-transport subpaths for drop-in client extension and transport-wrapping.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/valve-tech/gas-oracle#readme",
@@ -42,12 +42,17 @@
42
42
  },
43
43
  "files": [
44
44
  "dist",
45
+ "skills",
45
46
  "README.md",
47
+ "AGENTS.md",
48
+ "CHANGELOG.md",
46
49
  "LICENSE"
47
50
  ],
48
51
  "scripts": {
49
52
  "build": "tsc -p .",
50
53
  "typecheck": "tsc -p . --noEmit",
54
+ "typecheck:examples": "tsc -p examples",
55
+ "lint": "eslint src",
51
56
  "test": "vitest run",
52
57
  "test:watch": "vitest",
53
58
  "prepare": "yarn build"
@@ -57,7 +62,11 @@
57
62
  },
58
63
  "devDependencies": {
59
64
  "@types/node": "^22.0.0",
65
+ "@typescript-eslint/eslint-plugin": "^8.59.1",
66
+ "@typescript-eslint/parser": "^8.59.1",
67
+ "eslint": "^10.3.0",
60
68
  "typescript": "^6.0.2",
69
+ "typescript-eslint": "^8.59.1",
61
70
  "viem": "^2.21.0",
62
71
  "vitest": "^4.1.4"
63
72
  }
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: gas-oracle-integration
3
+ description: Integrate `@valve-tech/gas-oracle` into an EVM dapp or backend. Use when the user wants gas-tier recommendations (`slow` / `standard` / `fast` / `instant`), needs to set `maxPriorityFeePerGas` and `maxFeePerGas` for a transaction, hits stuck-tx detection requirements, or asks "how do I price a transaction" against a viem `PublicClient`. Also use when seeing imports from `@valve-tech/gas-oracle` and the user asks for help configuring it per chain (Ethereum, Base, Arbitrum, OP, PulseChain), or asks about `priorityFeeDecayCap`, `priorityModel`, `tipForBlockPosition`, viem-actions, or viem-transport.
4
+ ---
5
+
6
+ # Integrating `@valve-tech/gas-oracle`
7
+
8
+ Multi-tier gas-fee oracle for EVM chains. This skill is for AI agents
9
+ working in a project that imports the package — it grounds you in the
10
+ right configuration choices for the user's chain and the right
11
+ integration shape for their codebase.
12
+
13
+ ## Decision tree: which integration to use
14
+
15
+ ```
16
+ Is the user already passing a viem PublicClient around?
17
+ ├── Yes — use viem-actions (`client.extend(gasOracleActions(...))` or
18
+ │ the direct invocation `gasOracleActions(opts)(client)`).
19
+ │ Most ergonomic for app code.
20
+ └── No — does the user have wagmi/viem code that already calls
21
+ `client.getGasPrice()` / `eth_maxPriorityFeePerGas`?
22
+ ├── Yes — use viem-transport (`withGasOracle(transport, ...)`)
23
+ │ to intercept those methods at the RPC layer. Drop-in.
24
+ │ No call-site changes.
25
+ └── No — use the direct constructor `createGasOracle(opts)`.
26
+ Simplest. Read tiers via `oracle.getState()?.tiers`.
27
+ ```
28
+
29
+ ## Per-chain config (always required)
30
+
31
+ | Chain | `chainId` | `priorityModel` | `baseFeeLivenessBlocks` | Notes |
32
+ |---|---|---|---|---|
33
+ | Ethereum mainnet | 1 | `'eip1559'` | 6 | Validators burn base fee. |
34
+ | Base | 8453 | `'eip1559'` | 6 | Same as ETH. |
35
+ | Arbitrum One | 42161 | `'eip1559'` | 6 | |
36
+ | Optimism | 10 | `'eip1559'` | 6 | |
37
+ | PulseChain mainnet | 369 | `'flat'` | 6 | Validators charge tips. |
38
+ | PulseChain testnet v4 | 943 | `'flat'` | 6 | |
39
+ | Unknown / unsure | — | `'flat'` | 6 | Conservative; never under-counts. |
40
+
41
+ `priorityFeeDecayCap`: leave at default (`WAD/8` = 12.5%/block, EIP-1559
42
+ parity) unless you have a specific reason to tighten/loosen.
43
+
44
+ ## Anti-patterns to flag
45
+
46
+ When reviewing user code, watch for these and suggest fixes:
47
+
48
+ 1. **Multiple oracles per chain in the same process.** Construct once,
49
+ module-scope it. Each oracle runs a poll interval and holds state.
50
+ Two oracles for chain 1 = double the RPC traffic, no benefit.
51
+
52
+ 2. **`oracle.getState()` in a hot path that runs every render / every
53
+ request.** It's O(1) but you're wasting cache lines. Either subscribe
54
+ via `oracle.subscribe(cb)` and store the latest state in a module
55
+ variable, or cache the result yourself with a short TTL.
56
+
57
+ 3. **Reading `oracle.getState()` immediately after `oracle.start()`
58
+ without handling null.** First poll hasn't completed yet; tiers will
59
+ be missing. Fix: `await oracle.pollOnce()` after `start()` to seed
60
+ state synchronously, then it's safe to call `getState()`.
61
+
62
+ 4. **Using `priorityModel: 'eip1559'` on PulseChain or other tip-charging
63
+ chains.** Cuts the distribution to type-2+ samples only, but
64
+ PulseChain validators don't honor the type byte — they sort by tip
65
+ regardless. Result: under-published tier values, your tx loses to
66
+ legacy spam.
67
+
68
+ 5. **`keepMempoolSnapshot: true` on a chain whose RPC gates
69
+ `txpool_content`** (most public RPCs). Wastes a poll cycle's RPC
70
+ budget on a request that always errors. Set `false` until you have
71
+ a node you operate.
72
+
73
+ 6. **Calling `findTxInMempool` with a hash that's been confirmed.**
74
+ Confirmed txs are NOT in the mempool snapshot (it's pending+queued
75
+ only). Check `eth_getTransactionByHash` instead.
76
+
77
+ ## How to recognize this package in the user's code
78
+
79
+ ```ts
80
+ // Direct constructor
81
+ import { createGasOracle } from '@valve-tech/gas-oracle'
82
+
83
+ // viem-actions extension
84
+ import { gasOracleActions } from '@valve-tech/gas-oracle/viem-actions'
85
+
86
+ // viem-transport interception
87
+ import { withGasOracle } from '@valve-tech/gas-oracle/viem-transport'
88
+ ```
89
+
90
+ `package.json` will show `"@valve-tech/gas-oracle": "^0.2.x"` in dependencies.
91
+
92
+ ## Where to find more
93
+
94
+ - Full API + types: `node_modules/@valve-tech/gas-oracle/AGENTS.md`
95
+ - Runnable examples: `node_modules/@valve-tech/gas-oracle/examples/`
96
+ - Human-facing docs: `node_modules/@valve-tech/gas-oracle/README.md`
97
+ - Source (when types alone aren't enough): `node_modules/@valve-tech/gas-oracle/dist/`
98
+ (compiled JS + .d.ts) — sources aren't shipped, only built output.