<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BlockVectra</title>
    <description>The latest articles on DEV Community by BlockVectra (@blockvectra).</description>
    <link>https://dev.to/blockvectra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195569%2F51965796-1833-48e9-bdd2-2ea3ee44be66.png</url>
      <title>DEV Community: BlockVectra</title>
      <link>https://dev.to/blockvectra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blockvectra"/>
    <language>en</language>
    <item>
      <title>Four transactions, eight transfers: an ERC-20 ingestion regression fixture</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:58:24 +0000</pubDate>
      <link>https://dev.to/blockvectra/four-transactions-eight-transfers-an-erc-20-ingestion-regression-fixture-5fif</link>
      <guid>https://dev.to/blockvectra/four-transactions-eight-transfers-an-erc-20-ingestion-regression-fixture-5fif</guid>
      <description>&lt;p&gt;A transfer table keyed only by transaction hash can lose valid rows while every RPC call succeeds.&lt;/p&gt;

&lt;p&gt;Here is a small, real fixture that catches that mistake: &lt;strong&gt;eight ERC-20 Transfer logs across four Base transactions&lt;/strong&gt;. The per-transaction counts are &lt;strong&gt;1, 1, 2 and 4&lt;/strong&gt;. Put those logs into a dictionary keyed only by transaction hash and only four rows survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the check without an RPC key
&lt;/h2&gt;

&lt;p&gt;Download &lt;code&gt;cadd-raw-logs.jsonl&lt;/code&gt; from the &lt;a href="https://gist.github.com/gjwang/de103a7a92aed7355eadfb552ecbdf66/88a81d4f2326dd5f9ec56e9d0b8f25518bc913d9" rel="noopener noreferrer"&gt;fixed sample revision&lt;/a&gt;. Save the following as &lt;code&gt;verify-sample.py&lt;/code&gt; in the same directory and run it with Python 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;logs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt;
        &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cadd-raw-logs.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

&lt;span class="n"&gt;by_transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transactionHash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;event_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transactionHash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logIndex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_keys&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event_keys&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8 logs; 4 transactions; 8 distinct event keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code intentionally constructs the lossy transaction-keyed dictionary, then checks the event-level keys separately. No package installation, wallet, key or network request is needed once the sample is downloaded.&lt;/p&gt;

&lt;p&gt;For this single-chain fixture, use transaction hash &lt;strong&gt;and log index&lt;/strong&gt; to distinguish events. A dataset spanning chains should also carry chain identity. Keep block identity and removed/reorg handling as separate concerns: a distinct row key does not prove that an event is still canonical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sample actually contains
&lt;/h2&gt;

&lt;p&gt;BlockVectra's keyed Base endpoint returned these eight logs on November 8, 2026 at 04:25 UTC, for CADD at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0x16f93ebc5320c89efc8701577efe49d14a276a06
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request filtered the ERC-20 Transfer topic over the inclusive range &lt;strong&gt;53,758,000–53,758,999&lt;/strong&gt;. The sample includes the raw result entries and expected rows from the unchanged decoder in &lt;a href="https://github.com/SeanC66/Non-USD-Stablecoin-Analysis/blob/dc7d75e4f9b971162d4fc8f44fe6825f84f6afd6/ingest/base.py" rel="noopener noreferrer"&gt;this pinned upstream loader&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Only &lt;strong&gt;one&lt;/strong&gt; event was independently compared with &lt;a href="https://basescan.org/tx/0xb9efc107b60a23cfec3fedd03e194f92014e97f748cb739960cbaefe599096be#eventlog" rel="noopener noreferrer"&gt;BaseScan&lt;/a&gt;: block 53,758,011, log index 6, contract/from/to, and raw amount 57370140000000000000000. The other seven are provider-returned samples. This does not establish that the window contains every event.&lt;/p&gt;

&lt;p&gt;Keep raw amounts as integers or decimal strings. The fixture does not use floating-point conversion to normalize token amounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timestamp and receipt checks are separate
&lt;/h2&gt;

&lt;p&gt;At 05:02 UTC, two additional calls returned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;decimals() at latest: &lt;strong&gt;18&lt;/strong&gt;, agreeing with the project seed.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;one-element JSON-RPC batch&lt;/strong&gt; for block 53,758,011: timestamp &lt;strong&gt;1793941977&lt;/strong&gt;, or &lt;strong&gt;2026-11-06T05:12:57Z&lt;/strong&gt;, matching the independently saved BaseScan time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those request/response bodies are in &lt;code&gt;loader-method-requests-responses.json&lt;/code&gt; in the same sample. The header hash agrees with the earlier log response, which is a same-provider cross-check rather than independent canonical verification. One batch item does not establish compatibility for the loader's usual 50-item batches.&lt;/p&gt;

&lt;p&gt;The earlier lookup of that transaction's receipt returned null. That result remains &lt;a href="https://github.com/unibitorg/rpc-gateway/issues/1597#issuecomment-6717227553" rel="noopener noreferrer"&gt;tracked separately&lt;/a&gt;; the upstream log loader does not directly call receipts. Safe-head selection, timestamps for the other sample blocks, February-onward history and the full database backfill have not been validated here.&lt;/p&gt;

&lt;p&gt;This fixture is useful for a narrow regression: preserving multiple transfers in one transaction. It is not a backfill benchmark, a complete ingestion test or evidence of customer adoption.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why viem event logs break JSON.stringify—and how to keep uint256 exact</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:39:34 +0000</pubDate>
      <link>https://dev.to/blockvectra/why-viem-event-logs-break-jsonstringify-and-how-to-keep-uint256-exact-4jf6</link>
      <guid>https://dev.to/blockvectra/why-viem-event-logs-break-jsonstringify-and-how-to-keep-uint256-exact-4jf6</guid>
      <description>&lt;p&gt;A script can fetch event logs successfully and still crash when it prints them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Do not know how to serialize a BigInt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty result may pass the same code path. There is simply no decoded integer in &lt;code&gt;[]&lt;/code&gt; to trigger the failure.&lt;/p&gt;

&lt;p&gt;That is the distinction in &lt;a href="https://github.com/four-meme-community/four-meme-ai/issues/3" rel="noopener noreferrer"&gt;four-meme-ai issue #3&lt;/a&gt;: non-empty BSC event results broke JSON output in two scripts. We submitted &lt;a href="https://github.com/four-meme-community/four-meme-ai/pull/4" rel="noopener noreferrer"&gt;PR #4&lt;/a&gt; with a two-line serialization fix and regression tests. The PR is open as of October 26, 2026; it has not been accepted upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep decoded integers exact
&lt;/h2&gt;

&lt;p&gt;In this case, viem decodes the events' &lt;code&gt;uint256&lt;/code&gt; arguments as JavaScript &lt;code&gt;bigint&lt;/code&gt;. Raw JSON-RPC logs contain hex strings; the BigInts appear in the decoded JavaScript values. A successful RPC response does not guarantee that those values can be passed directly to &lt;code&gt;JSON.stringify&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use a replacer at the output boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9007199254740993&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bigint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { "amount": "9007199254740993", "fee": "0" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The replacer also reaches BigInts nested inside event arrays and argument objects. It leaves ordinary strings, booleans and existing numbers unchanged. This addresses BigInts; it does not make circular references or other unsupported structures serializable.&lt;/p&gt;

&lt;p&gt;Avoid converting token amounts to &lt;code&gt;Number&lt;/code&gt; just to make JSON work. For example, &lt;code&gt;Number(9007199254740993n)&lt;/code&gt; becomes &lt;code&gt;9007199254740992&lt;/code&gt;. The exception disappears, but the value has changed. MDN explains the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/BigInt_not_serializable" rel="noopener noreferrer"&gt;BigInt serialization error and replacer options&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The JSON contract now contains decimal strings. A consumer that knows a field represents an integer can use &lt;code&gt;BigInt(parsed.amount)&lt;/code&gt; for arithmetic. Do not convert every decimal-looking string indiscriminately: identifiers and other textual fields may also contain digits. Decimal places still come from the token's metadata, not from serialization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test a non-empty result, an empty result, and an error
&lt;/h2&gt;

&lt;p&gt;The patch's tests run both actual TypeScript scripts against a loopback JSON-RPC server:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case, for each script&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Four synthetic event types with zero, values above the safe-integer limit, and &lt;code&gt;uint256&lt;/code&gt; max&lt;/td&gt;
&lt;td&gt;Valid JSON with exact decimal-string arguments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty log array&lt;/td&gt;
&lt;td&gt;Successful empty output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPC error&lt;/td&gt;
&lt;td&gt;Non-zero exit, without empty-success output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The event types are &lt;code&gt;TokenCreate&lt;/code&gt;, &lt;code&gt;TokenPurchase&lt;/code&gt;, &lt;code&gt;TokenSale&lt;/code&gt; and &lt;code&gt;LiquidityAdded&lt;/code&gt;. Each non-empty test checks all four; these are six tests total, not eight separate event tests.&lt;/p&gt;

&lt;p&gt;Before the fix, both non-empty tests failed: four passed and two failed. After the fix, all six passed. The TypeScript build also passed. The fixtures are synthetic and encoded with viem, so this verifies the scripts' serialization and error behavior—not the ABI against an independent source, a live BSC provider, or the complete CLI wrapper.&lt;/p&gt;

&lt;p&gt;To run the same tests from the proposed patch, install Node.js 24 and check out the fixed commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; fix/event-bigint-json https://github.com/gjwang/four-meme-ai.git
&lt;span class="nb"&gt;cd &lt;/span&gt;four-meme-ai
git checkout e797b768307250ac59065dd646739c4750dc3e3d
npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;
node &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/events-json.test.mjs
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependency installation needs network access. The regression tests use local synthetic responses and need no RPC key, wallet or transaction.&lt;/p&gt;

&lt;p&gt;For a log reader, transport errors, valid empty results and output failures need separate handling. If you are also building a historical scan, our &lt;a href="https://docs.blockvectra.com/en/guides/getlogs-block-range/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=bigint-event-json" rel="noopener noreferrer"&gt;eth_getLogs block-range and recovery guide&lt;/a&gt; covers bounded queries and saved progress. Fixing serialization does not prove that an empty response is complete, and this patch does not change the default BSC RPC provider.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>web3</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Build a Robinhood Chain stock-token activity dashboard with one API request</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:38:40 +0000</pubDate>
      <link>https://dev.to/blockvectra/build-a-robinhood-chain-stock-token-activity-dashboard-with-one-api-request-2emi</link>
      <guid>https://dev.to/blockvectra/build-a-robinhood-chain-stock-token-activity-dashboard-with-one-api-request-2emi</guid>
      <description>&lt;p&gt;To build a daily stock-token activity view on Robinhood Chain, start with transfer counts and holder counts for the latest recorded UTC day. The example below fetches them with one authenticated Data API request. Its default run makes no JSON-RPC calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the first request
&lt;/h2&gt;

&lt;p&gt;Use Node.js 24 or newer. Clone the example, install locked dependencies, and export a key in your own shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/blockvectra/robinhood-stock-tokens.git
&lt;span class="nb"&gt;cd &lt;/span&gt;robinhood-stock-tokens
npm ci
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BLOCKVECTRA_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'replace-with-your-key'&lt;/span&gt;
npm start &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--limit&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://blockvectra.com/en/get-api-key/?ref=dev-stock-activity" rel="noopener noreferrer"&gt;Get a BlockVectra API key&lt;/a&gt; and keep it in your environment; do not paste it into chat or commit it. Installation downloads are setup work, not API runtime traffic. The first &lt;code&gt;npm start&lt;/code&gt; calls &lt;code&gt;GET /v1/data/robinhood_mainnet/stocks?limit=5&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read what comes back
&lt;/h2&gt;

&lt;p&gt;Omitting &lt;code&gt;--day&lt;/code&gt; asks for the latest recorded day, not today's date or a live quote. The CLI prints the requested day, response metadata, then rows with the recorded Day and token Address alongside activity fields. The API rows include &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;symbol&lt;/code&gt;, &lt;code&gt;transfers&lt;/code&gt;, and &lt;code&gt;holder_count&lt;/code&gt;; the &lt;a href="https://docs.blockvectra.com/en/guides/stocks/?ref=dev-stock-activity" rel="noopener noreferrer"&gt;stock token data guide&lt;/a&gt; documents the full response.&lt;/p&gt;

&lt;p&gt;This is a capped list of indexed activity, with up to five rows for this command. The default budget is one Data GET and zero RPC calls, and the stocks leaderboard has no cursor pagination. Each request has a 10-second timeout, and the CLI does not retry. &lt;code&gt;meta.refreshed_at&lt;/code&gt; is the snapshot's refresh time. If it is missing or null, the CLI prints &lt;code&gt;unknown&lt;/code&gt; and says to treat the data as stale. &lt;code&gt;meta.as_of_block&lt;/code&gt; is the current indexed watermark; it does not prove that the daily table is fresh or complete.&lt;/p&gt;

&lt;p&gt;An empty &lt;code&gt;data&lt;/code&gt; array means there are no recorded rows for that request. That can be a normal quiet day; it does not, by itself, prove the ingestion pipeline is broken. Conversely, a populated response does not guarantee every token is covered. Check the actual day and freshness fields before using the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the multiplier only when needed
&lt;/h2&gt;

&lt;p&gt;Pass &lt;code&gt;--with-multiplier&lt;/code&gt; to create an RPC client. The CLI first checks &lt;code&gt;eth_chainId&lt;/code&gt; equals &lt;code&gt;4663&lt;/code&gt;, then reads &lt;code&gt;uiMultiplier()&lt;/code&gt; for at most the first 10 rows, sequentially and without retries. That optional path can use at most 12 runtime requests: one Data GET, one chain-ID check, and ten calls. A chain mismatch or unverifiable chain ID exits before any &lt;code&gt;eth_call&lt;/code&gt;; a failed per-token read appears as &lt;code&gt;unavailable&lt;/code&gt;, and rows past the cap appear as &lt;code&gt;not queried (limit 10)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The multiplier is current contract state, not the historical value for a past &lt;code&gt;day&lt;/code&gt;. It cannot supply the historical multiplier needed to calculate adjusted values for that past date. For the exact implementation, see the &lt;a href="https://github.com/blockvectra/robinhood-stock-tokens/tree/12f341c41a608df2e302a0a98cdcc2d02c869f8e" rel="noopener noreferrer"&gt;source at commit 12f341c&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ethereum</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Diagnosing eth_getLogs Returning [] When the Receipt Shows the Event</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Sun, 25 Oct 2026 17:28:40 +0000</pubDate>
      <link>https://dev.to/blockvectra/diagnosing-ethgetlogs-returning-when-the-receipt-shows-the-event-5102</link>
      <guid>https://dev.to/blockvectra/diagnosing-ethgetlogs-returning-when-the-receipt-shows-the-event-5102</guid>
      <description>&lt;p&gt;An empty &lt;code&gt;eth_getLogs&lt;/code&gt; result can be valid, but a successful response does not establish that the provider served a complete view. If a receipt contains the event you expected, compare the chain, block and exact filter before changing providers.&lt;/p&gt;

&lt;p&gt;The pattern is familiar from &lt;a href="https://github.com/bnb-chain/bsc/issues/3853" rel="noopener noreferrer"&gt;BSC #3853&lt;/a&gt;, where a reporter described &lt;code&gt;eth_getLogs&lt;/code&gt; returning &lt;code&gt;[]&lt;/code&gt; against legacy BSC endpoints despite receipt evidence. That is their observation, not a cause we reproduced. It reminds us to separate what a receipt proves from what a log query returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the query before the endpoint
&lt;/h2&gt;

&lt;p&gt;Start with the exact chain. A filter that looks right on BSC mainnet (chain ID 56) can be meaningless on a testnet or another EVM chain. Then compare the same block: the receipt's &lt;code&gt;blockHash&lt;/code&gt; against the &lt;code&gt;blockHash&lt;/code&gt; in returned logs, and whether you are looking at the same canonical block or a stale, reorged view. The sample below is a single fixed-block observation; it does not independently prove canonicality. Confirm the exact address and every topic position, including case normalization and whether you intended a wildcard. Finally, check whether the endpoint supports the method and range you requested. An explicit rejection is different from a successful empty result; policy alone does not explain why a particular query returned no logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reproducible fixed-block sample
&lt;/h2&gt;

&lt;p&gt;BlockVectra operates BSC infrastructure. Our keyed BSC internal QA observed this at UTC 2026-10-25T13:26:21.586695Z. Three serial HTTP/RPC calls ran: &lt;code&gt;eth_chainId&lt;/code&gt;, &lt;code&gt;eth_getTransactionReceipt&lt;/code&gt;, then &lt;code&gt;eth_getLogs&lt;/code&gt;. &lt;code&gt;chainId&lt;/code&gt; returned chain 56. The receipt was for a transaction in fixed block 129476271 (&lt;code&gt;0x7b7a6af&lt;/code&gt;). The &lt;code&gt;getLogs&lt;/code&gt; filter used only the contract address and that fixed block range, with no topics. It returned four logs. One selected ERC-721 Transfer for token 1811 matches the receipt event across &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;topics&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;blockHash&lt;/code&gt;, &lt;code&gt;blockNumber&lt;/code&gt;, &lt;code&gt;transactionHash&lt;/code&gt;, &lt;code&gt;transactionIndex&lt;/code&gt;, and &lt;code&gt;logIndex&lt;/code&gt;. The other three are not individually receipt-validated here.&lt;/p&gt;

&lt;p&gt;The saved check runs offline, with no key or network after download. Download the &lt;a href="https://gist.github.com/gjwang/5a8f80f5e441a5701e2e01ae73f187bf/archive/cbf598b79c846211e6788c727edac491e79275f5.zip" rel="noopener noreferrer"&gt;pinned ZIP snapshot&lt;/a&gt;, extract its ten files, open the extracted directory, and run with Node.js 22+:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node verify-sample.mjs observed-sample.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS 1 receipt event matched among 4 returned logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live requests are separate: they need your endpoint, permissions, and method policy. Never paste API keys or other secrets into issue comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the saved response can and cannot tell you
&lt;/h2&gt;

&lt;p&gt;This is a consistency check within one saved observation. It does not prove independent canonicality, cover full history, explain the original incident, or measure speed. Use it to verify that a known receipt event and a fixed-block log response can be reconciled, then apply the same checks—chain, block hash, exact filter, method policy—to your own case.&lt;/p&gt;

&lt;p&gt;If this is useful, start with the &lt;a href="https://gist.github.com/gjwang/5a8f80f5e441a5701e2e01ae73f187bf/cbf598b79c846211e6788c727edac491e79275f5" rel="noopener noreferrer"&gt;fixed artifact revision&lt;/a&gt; and the public &lt;a href="https://github.com/bnb-chain/bsc/issues/3853" rel="noopener noreferrer"&gt;BSC #3853&lt;/a&gt; thread. Both are public.&lt;/p&gt;

&lt;p&gt;For a live comparison, the snapshot also includes an optional client that makes up to three requests to your chosen provider without retries. Read its README before using it. &lt;a href="https://blockvectra.com/en/chains/bsc_mainnet/?ref=dev-bsc-sample-1108" rel="noopener noreferrer"&gt;BSC connection options&lt;/a&gt; are available for BlockVectra; access policy and historical retention can differ from the saved observation.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>debugging</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Read Robinhood Chain mainnet RPC without an API key (TypeScript)</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:06:04 +0000</pubDate>
      <link>https://dev.to/blockvectra/read-robinhood-chain-mainnet-rpc-without-an-api-key-typescript-2ml6</link>
      <guid>https://dev.to/blockvectra/read-robinhood-chain-mainnet-rpc-without-an-api-key-typescript-2ml6</guid>
      <description>&lt;p&gt;You can read Robinhood Chain mainnet without an API key. The script is TypeScript and requires Node.js 18 or newer. Clone the public quickstart, install dependencies, and run its public-read script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/blockvectra/agent-quickstart.git
&lt;span class="nb"&gt;cd &lt;/span&gt;agent-quickstart
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run &lt;span class="nb"&gt;read&lt;/span&gt;:public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command first fetches &lt;code&gt;https://api.blockvectra.com/v1/chains&lt;/code&gt;. It requires a &lt;code&gt;robinhood_mainnet&lt;/code&gt; entry whose &lt;code&gt;public.url&lt;/code&gt; is exactly &lt;code&gt;https://api.blockvectra.com/v1/robinhood_mainnet/public&lt;/code&gt;, and whose &lt;code&gt;public.methods&lt;/code&gt; list contains &lt;code&gt;eth_chainId&lt;/code&gt; and &lt;code&gt;eth_blockNumber&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That directory check happens before any RPC call. If the endpoint or either method is no longer advertised, the script stops. Otherwise it calls &lt;code&gt;eth_chainId&lt;/code&gt; and verifies &lt;code&gt;4663&lt;/code&gt; before reading the latest block. It makes at most three HTTP requests: one directory read and two JSON-RPC calls, with no retries, a 15-second timeout and a 128 KiB response cap per call. No wallet, private key, login, account or key creation is involved, and the block number stays an exact decimal string.&lt;/p&gt;

&lt;p&gt;One internal QA run of the &lt;a href="https://github.com/blockvectra/agent-quickstart/tree/cac7799148e0882f48d474c5896f1fdf73de3d63" rel="noopener noreferrer"&gt;pinned version&lt;/a&gt;, observed on 2026-10-25 at 15:24 UTC, returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"chain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"robinhood_mainnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"chainId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4663&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"blockNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"97687425"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Block height changes. That block number does not establish finality, historical data or data freshness. This is mainnet chain 4663, not testnet 46630.&lt;/p&gt;

&lt;p&gt;Do not confuse &lt;code&gt;npm run read:public&lt;/code&gt; with &lt;code&gt;npm start&lt;/code&gt;, which runs full SIWE/wallet/key onboarding and may create an account or key. The Data API and account operations remain keyed.&lt;/p&gt;

&lt;p&gt;For the project's own setup and usage notes, see the existing &lt;a href="https://github.com/blockvectra/agent-quickstart/blob/cac7799148e0882f48d474c5896f1fdf73de3d63/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;. The public-read source is pinned at &lt;a href="https://github.com/blockvectra/agent-quickstart/blob/cac7799148e0882f48d474c5896f1fdf73de3d63/src/public-read.ts" rel="noopener noreferrer"&gt;src/public-read.ts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Install the native BlockVectra MCP plugin in Antigravity</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:53:49 +0000</pubDate>
      <link>https://dev.to/blockvectra/install-the-native-blockvectra-mcp-plugin-in-antigravity-3gck</link>
      <guid>https://dev.to/blockvectra/install-the-native-blockvectra-mcp-plugin-in-antigravity-3gck</guid>
      <description>&lt;h2&gt;
  
  
  Install in one project
&lt;/h2&gt;

&lt;p&gt;Run these commands from your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .agents/plugins/blockvectra-docs
curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; https://raw.githubusercontent.com/blockvectra/mcp/antigravity-v0.1.0/plugin.json &lt;span class="nt"&gt;--output&lt;/span&gt; .agents/plugins/blockvectra-docs/plugin.json
curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; https://raw.githubusercontent.com/blockvectra/mcp/antigravity-v0.1.0/mcp_config.json &lt;span class="nt"&gt;--output&lt;/span&gt; .agents/plugins/blockvectra-docs/mcp_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start &lt;code&gt;agy&lt;/code&gt; in that directory. For a first check, ask it to call &lt;code&gt;search_docs&lt;/code&gt; with &lt;code&gt;{"query":"Antigravity","lang":"en","limit":1}&lt;/code&gt;. Project-local loading and this single keyless documentation search were verified with Antigravity CLI 1.4.7.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.antigravity.google/docs/plugins/" rel="noopener noreferrer"&gt;official plugin guide&lt;/a&gt; describes project-local and global installation. The commands above place the plugin in this project's &lt;code&gt;.agents/plugins/&lt;/code&gt; directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI-managed global installation
&lt;/h2&gt;

&lt;p&gt;This walkthrough assumes Antigravity CLI is already installed and signed in. Install the BlockVectra plugin from its GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agy plugin &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/blockvectra/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin is named &lt;code&gt;blockvectra-docs&lt;/code&gt;. It connects to &lt;code&gt;https://docs.blockvectra.com/mcp&lt;/code&gt; over remote HTTP, so there is no local npm bridge to start or keep running. If you want to confirm what Antigravity recorded, the optional inventory command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agy plugin list &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the plugin appears as enabled and note its version and source. If the install command fails, fix that before relying on the plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check what the client loaded
&lt;/h2&gt;

&lt;p&gt;For a CLI-managed installation, restart Antigravity, run &lt;code&gt;/plugin&lt;/code&gt; to confirm &lt;code&gt;blockvectra-docs&lt;/code&gt; is enabled, then &lt;code&gt;/mcp&lt;/code&gt; to inspect the server and the tools it exposes. For the project-local setup, start Antigravity from that project directory and inspect &lt;code&gt;/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keep two levels of success separate. The lists in &lt;code&gt;/plugin&lt;/code&gt; and &lt;code&gt;/mcp&lt;/code&gt; confirm that the client registered and loaded the plugin; they do not prove the remote endpoint is reachable right now. The client's live server status shows whether the connection is up, and a tool has worked only when a call returns the data you asked for. Follow the client's normal tool permission prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a keyless discovery check
&lt;/h2&gt;

&lt;p&gt;Ask Antigravity to make two calls in plain language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Call list_chains, then call read_doc with path=quickstart and lang=en.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful &lt;code&gt;list_chains&lt;/code&gt; response confirms chain discovery. A successful &lt;code&gt;read_doc&lt;/code&gt; response confirms documentation retrieval. Both are keyless. Connecting, reading docs, discovery, pricing, and status tools do not need an API key.&lt;/p&gt;

&lt;p&gt;That does not make every RPC method keyless. Public methods vary by chain, so check the relevant chain's policy before assuming a call will run without credentials. Data API and account tools need your own key. Configure it only in your MCP client's private HTTP headers (&lt;code&gt;x-api-key&lt;/code&gt; or &lt;code&gt;Authorization&lt;/code&gt;); never put it in command arguments, tool arguments, or chat.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;/mcp&lt;/code&gt; does not list the server, check &lt;code&gt;/plugin&lt;/code&gt;, the install output, and whether the client was restarted after install. If &lt;code&gt;read_doc&lt;/code&gt; fails, check the &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;lang&lt;/code&gt; arguments, then read the tool's error. For background and the full tool list, see the &lt;a href="https://docs.blockvectra.com/en/guides/mcp-server/?ref=dev-antigravity-native" rel="noopener noreferrer"&gt;BlockVectra MCP server guide&lt;/a&gt;. Antigravity's own documentation covers &lt;a href="https://www.antigravity.google/docs/plugins?tab=cli" rel="noopener noreferrer"&gt;plugins&lt;/a&gt; and &lt;a href="https://www.antigravity.google/docs/mcp/" rel="noopener noreferrer"&gt;MCP configuration&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>antigravity</category>
      <category>mcp</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
    <item>
      <title>Resume ERC-20 transfer history without skipping a failed page</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Sat, 24 Oct 2026 18:05:04 +0000</pubDate>
      <link>https://dev.to/blockvectra/resume-erc-20-transfer-history-without-skipping-a-failed-page-3knd</link>
      <guid>https://dev.to/blockvectra/resume-erc-20-transfer-history-without-skipping-a-failed-page-3knd</guid>
      <description>&lt;p&gt;A transfer-history export should resume from its last saved page after a timeout. It should not start over, and it should not advance past a page it never received.&lt;/p&gt;

&lt;p&gt;The useful unit of progress is &lt;strong&gt;the response and its next cursor, saved together&lt;/strong&gt;. A cursor on its own tells you where to continue, but not whether you kept the data that came before it.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://github.com/blockvectra/data-api-js/blob/fa40d51e322fe94683b4f184289ce48e39ec825f/examples/address-transfers-page.mjs" rel="noopener noreferrer"&gt;BlockVectra example&lt;/a&gt; reads ERC-20 transfers for one address in a fixed block window. It makes at most one request per invocation, saves the original response envelope, and resumes when you run it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the packaged example
&lt;/h2&gt;

&lt;p&gt;The example is included in the &lt;code&gt;0.1.5&lt;/code&gt; GitHub release archive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/blockvectra/data-api-js/releases/download/v0.1.5/blockvectra-data-api-0.1.5.tgz
&lt;span class="nb"&gt;umask &lt;/span&gt;077
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .checkpoints
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 .checkpoints
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this archive for the example. The npm &lt;code&gt;0.1.4&lt;/code&gt; package does not include it.&lt;/p&gt;

&lt;p&gt;Provide these environment variables before running:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLOCKVECTRA_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your &lt;a href="https://blockvectra.com/en/get-api-key/?ref=dev-checkpoint" rel="noopener noreferrer"&gt;BlockVectra API key&lt;/a&gt;, supplied through your environment or secret manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLOCKVECTRA_CHAIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A chain slug supporting the transfers dataset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLOCKVECTRA_ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The address whose ERC-20 transfers you want&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLOCKVECTRA_FROM_BLOCK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;First block, as a non-negative decimal integer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLOCKVECTRA_TO_BLOCK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Last block, inclusive; the window must contain at most 100,000 blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use a fixed historical window for this run. The example does not look up the latest head or silently move the bounds. Check &lt;a href="https://docs.blockvectra.com/en/api/data/?ref=dev-checkpoint#chains" rel="noopener noreferrer"&gt;dataset support and coverage&lt;/a&gt; for the chain you choose.&lt;/p&gt;

&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BLOCKVECTRA_STATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/.checkpoints/transfers.json"&lt;/span&gt;
node node_modules/@blockvectra/data-api/examples/address-transfers-page.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the same command again, with the same inputs, to request the next page. Keep the checkpoint directory private and outside version control. The API key stays outside the checkpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when a page fails?
&lt;/h2&gt;

&lt;p&gt;Suppose page one is saved and page two times out. The saved page and cursor remain unchanged. A later invocation tries from that same cursor; it does not skip ahead or reset to page one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Checkpoint behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valid response with a next cursor&lt;/td&gt;
&lt;td&gt;Save the response and cursor together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP error, timeout or malformed response&lt;/td&gt;
&lt;td&gt;Stop without advancing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different address, chain or block bounds&lt;/td&gt;
&lt;td&gt;Reject the mismatched checkpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saved last page has no next cursor&lt;/td&gt;
&lt;td&gt;Exit without sending another request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is a 15-second timeout and no automatic retry. You decide when to run again. Cursor values are opaque: do not decode, edit or manufacture the next one.&lt;/p&gt;

&lt;p&gt;The example fixes ERC-20, both transfer directions and 100 rows per page. It stops at 20 saved pages and also bounds response and checkpoint sizes. These are limits of this example, not service-wide guarantees. A stale lock needs deliberate review; the script does not remove it automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination finished is not history complete
&lt;/h2&gt;

&lt;p&gt;The checkpoint keeps each page's original &lt;code&gt;meta&lt;/code&gt; fields. If coverage is &lt;code&gt;partial&lt;/code&gt;, it stays &lt;code&gt;partial&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even &lt;code&gt;coverage=full&lt;/code&gt; and an exhausted cursor do not independently prove that the upstream index has no gaps. An ascending backfill can still have a hole that returns HTTP 200 with empty data. Treat pagination state, dataset coverage and your application's completeness checks as separate evidence.&lt;/p&gt;

&lt;p&gt;Checkpoint replacement is atomic for process interruption; it is not a power-loss, reorg-recovery or snapshot-isolation guarantee. Run one invocation per checkpoint at a time.&lt;/p&gt;

&lt;p&gt;The published archive was installed and checked with mocked pages, including cursor resume, mismatched inputs, a completed checkpoint and preserved &lt;code&gt;partial&lt;/code&gt; coverage. That verifies the example's local behavior, not the completeness of a live transfer history.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/blockvectra/data-api-js/blob/fa40d51e322fe94683b4f184289ce48e39ec825f/README.md#checkpointed-erc-20-transfers-one-page-per-run" rel="noopener noreferrer"&gt;source and setup notes&lt;/a&gt; explain the bounds if you want to adapt it for a scheduled export.&lt;/p&gt;

&lt;p&gt;Working directly with RPC logs instead? The &lt;a href="https://dev.to/blockvectra/resumable-ethgetlogs-scans-fixed-chunks-saved-checkpoints-safe-restarts-3b3p"&gt;resumable eth_getLogs guide&lt;/a&gt; covers fixed block chunks; the transfer example above follows an indexed API cursor.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Check the key before the first Data API request</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Fri, 23 Oct 2026 02:03:25 +0000</pubDate>
      <link>https://dev.to/blockvectra/check-the-key-before-the-first-data-api-request-728</link>
      <guid>https://dev.to/blockvectra/check-the-key-before-the-first-data-api-request-728</guid>
      <description>&lt;p&gt;An unset environment variable is still &lt;code&gt;undefined&lt;/code&gt; at runtime, even when TypeScript code uses a non-null assertion. Catching that configuration error before a request makes the first call easier to debug.&lt;/p&gt;

&lt;p&gt;The BlockVectra Data API TypeScript client now rejects a missing, empty or whitespace-only key when you create the client. It leaves non-empty keys unchanged; the API checks whether they are active and valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: check public RPC before Data API setup
&lt;/h2&gt;

&lt;p&gt;For a first connectivity check, clone the &lt;a href="https://github.com/blockvectra/agent-quickstart" rel="noopener noreferrer"&gt;TypeScript quickstart&lt;/a&gt; and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/blockvectra/agent-quickstart.git
&lt;span class="nb"&gt;cd &lt;/span&gt;agent-quickstart
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run &lt;span class="nb"&gt;read&lt;/span&gt;:public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command reads the public chain directory, checks that the advertised Robinhood Mainnet endpoint supports the two required methods, verifies chain ID &lt;code&gt;4663&lt;/code&gt;, and reads the latest block number. It makes at most three HTTP requests, with no retries. No wallet, account, or API key is needed for this path.&lt;/p&gt;

&lt;p&gt;This is a connectivity check; a block number does not establish historical coverage or Data API freshness. For Data API calls and account operations, continue with the keyed onboarding flow below. &lt;code&gt;npm start&lt;/code&gt; remains the full wallet/SIWE/key flow and can create an account and API key.&lt;/p&gt;

&lt;p&gt;Version 0.1.4 is available on &lt;a href="https://www.npmjs.com/package/@blockvectra/data-api" rel="noopener noreferrer"&gt;npm&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @blockvectra/data-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package includes JavaScript and TypeScript declarations; no local build is needed.&lt;/p&gt;

&lt;p&gt;Here is a Node.js TypeScript example. Keep the key in server configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDataClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDataApiError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@blockvectra/data-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLOCKVECTRA_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set BLOCKVECTRA_API_KEY on the server.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDataClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/{chain}/status/freshness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;robinhood_mainnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isDataApiError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unstructured_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Data API &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;as_of_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;as_of_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;refreshed_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshed_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;isDataApiError&lt;/code&gt; matters because an HTTP error body can be JSON, plain text, HTML or empty. The guard checks the Data API JSON envelope before the example reads &lt;code&gt;error.error.code&lt;/code&gt;. It does not parse strings, rewrite the response or decide whether the key is valid. The original HTTP status remains available in every response.&lt;/p&gt;

&lt;p&gt;The same pattern is useful for a backend service or an agent making the request. Keep the response metadata with the result, then check the dataset's coverage and refresh state against the task you are doing. An empty result or a successful status alone does not establish that the expected data is complete and current.&lt;/p&gt;

&lt;p&gt;For v0.1.3, validation covered 37 unit tests, type checking and all nine README TypeScript examples. The prebuilt package was also installed and checked in a separate project with mock responses. The credentialed live test was skipped; this example is not a report of a new live API run.&lt;/p&gt;

&lt;p&gt;Get a key through the &lt;a href="https://blockvectra.com/en/get-api-key/?ref=dev-data-api-sdk" rel="noopener noreferrer"&gt;normal key setup&lt;/a&gt;, and use the &lt;a href="https://github.com/blockvectra/data-api-js#readme" rel="noopener noreferrer"&gt;SDK README&lt;/a&gt; for pagination and other endpoints. The &lt;a href="https://github.com/blockvectra/data-api-js/releases/tag/v0.1.4" rel="noopener noreferrer"&gt;v0.1.4 release&lt;/a&gt; includes the archive and its SHA-256 checksums.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resume an ERC-20 transfer-history query
&lt;/h2&gt;

&lt;p&gt;For a query that spans several pages, the &lt;a href="https://github.com/blockvectra/data-api-js/blob/fa40d51e322fe94683b4f184289ce48e39ec825f/examples/address-transfers-page.mjs" rel="noopener noreferrer"&gt;checkpointed transfer example&lt;/a&gt; saves each response with its opaque next cursor. Run it again with the same address, block range and checkpoint to read the next page. Each invocation makes at most one request, with a 15-second timeout and no automatic retry. A failed page leaves the saved checkpoint unchanged; a finished checkpoint makes no further request.&lt;/p&gt;

&lt;p&gt;The example is included in the &lt;a href="https://github.com/blockvectra/data-api-js/releases/tag/v0.1.5" rel="noopener noreferrer"&gt;0.1.5 GitHub archive&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/blockvectra/data-api-js/releases/download/v0.1.5/blockvectra-data-api-0.1.5.tgz
node node_modules/@blockvectra/data-api/examples/address-transfers-page.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before running, set the API key, chain, address, fixed block bounds and checkpoint path using the &lt;a href="https://github.com/blockvectra/data-api-js/blob/fa40d51e322fe94683b4f184289ce48e39ec825f/README.md#checkpointed-erc-20-transfers-one-page-per-run" rel="noopener noreferrer"&gt;setup and limits&lt;/a&gt;. Use an existing private checkpoint directory. The older npm &lt;code&gt;0.1.4&lt;/code&gt; package does not include this example; the archive URL above installs &lt;code&gt;0.1.5&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;Pagination exhausted only means there is no next cursor. It does not prove that the upstream index has no gaps, and a &lt;code&gt;partial&lt;/code&gt; coverage stays &lt;code&gt;partial&lt;/code&gt;. Keep the API key in the environment, outside the checkpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Deno 2
&lt;/h2&gt;

&lt;p&gt;For the Deno CLI, start with the &lt;a href="https://github.com/blockvectra/data-api-js/blob/4aee8df52c0f45b51b974124e8f46c72d8557520/README.md#install-the-prebuilt-package" rel="noopener noreferrer"&gt;prebuilt package installation&lt;/a&gt;, then save the &lt;a href="https://github.com/blockvectra/data-api-js/blob/4aee8df52c0f45b51b974124e8f46c72d8557520/README.md#deno-quick-start" rel="noopener noreferrer"&gt;Deno quick-start example&lt;/a&gt; as &lt;code&gt;quickstart-deno.ts&lt;/code&gt;. It reads the key from your environment and requests the dataset freshness endpoint once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deno run &lt;span class="nt"&gt;--node-modules-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;manual &lt;span class="nt"&gt;--no-prompt&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;BLOCKVECTRA_API_KEY,BLOCKVECTRA_CHAIN &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-net&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api.blockvectra.com quickstart-deno.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This grants access to two environment variables and the API host. An empty key fails before a request; the request has a 15-second timeout and no retries. Check the returned dataset watermarks before using the data.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>blockchain</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Smithery's October 2026 Change: Point BlockVectra MCP Directly</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:27:17 +0000</pubDate>
      <link>https://dev.to/blockvectra/smitherys-october-2026-change-point-blockvectra-mcp-directly-2gmo</link>
      <guid>https://dev.to/blockvectra/smitherys-october-2026-change-point-blockvectra-mcp-directly-2gmo</guid>
      <description>&lt;p&gt;Smithery has announced that on October 31, 2026, it will stop connection management. According to its &lt;a href="https://smithery.ai/future" rel="noopener noreferrer"&gt;official announcement&lt;/a&gt;, that change covers the &lt;code&gt;run.tools&lt;/code&gt; and &lt;code&gt;server.smithery.ai&lt;/code&gt; proxy endpoints, managed identity, accounts and API keys, the API/Connect/Toolbox surfaces, and its CLI and SDK. Its public, non-proxy MCP registry will remain.&lt;/p&gt;

&lt;p&gt;If your client uses Smithery to reach BlockVectra, plan to point it directly at the vendor endpoint instead. This is not a universal replacement for every MCP server: only BlockVectra's own MCP endpoint is covered here, and other servers should move to their respective official endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the direct BlockVectra endpoint
&lt;/h2&gt;

&lt;p&gt;The endpoint is &lt;code&gt;https://docs.blockvectra.com/mcp&lt;/code&gt;. The current guide describes a stateless Streamable HTTP transport using JSON-RPC 2.0 over POST. Update your existing client entry rather than adding a duplicate connection. Config keys differ across clients, so use the guide's client-specific examples rather than copying a universal JSON shape or inventing local CLI flags.&lt;/p&gt;

&lt;p&gt;A browser &lt;code&gt;GET&lt;/code&gt; to the endpoint returns &lt;code&gt;405&lt;/code&gt;. That response alone does not prove an outage; the endpoint expects POST.&lt;/p&gt;

&lt;p&gt;To list tools without credentials, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json, text/event-stream'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/list"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://docs.blockvectra.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lists tools; it does not prove that a business RPC will succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify before removing the old connection
&lt;/h2&gt;

&lt;p&gt;Connecting, listing tools and reading documentation need no API key. As one small client-side check, call the &lt;code&gt;read_doc&lt;/code&gt; tool with arguments &lt;code&gt;path=quickstart&lt;/code&gt; and &lt;code&gt;lang=en&lt;/code&gt;. It reads documentation only. Check for JSON-RPC errors and the tool's &lt;code&gt;isError&lt;/code&gt; value, not just HTTP 200.&lt;/p&gt;

&lt;p&gt;Indexed data and account tools need your own BlockVectra API key in the client's HTTP &lt;code&gt;x-api-key&lt;/code&gt; header; &lt;code&gt;Authorization: Bearer&lt;/code&gt; is also documented. Public RPC eligibility depends on the actual chain's &lt;code&gt;public.methods&lt;/code&gt;. Do not assume every tool, transaction or dataset is keyless, and do not hard-code a tool count.&lt;/p&gt;

&lt;p&gt;Do not carry a Smithery key into BlockVectra headers. Never place an API key or private key in tool arguments, URLs or chat. Avoid changing global permissions as a shortcut.&lt;/p&gt;

&lt;p&gt;Complete the client handshake, tool list and doc read first. Then separately validate any authenticated or business tasks against the chains and methods you actually need before deleting the old connection. A successful doc read does not establish business-RPC or dataset coverage.&lt;/p&gt;

&lt;p&gt;Client install choices and auth details: &lt;a href="https://docs.blockvectra.com/en/guides/mcp-server/?ref=devto-smithery-direct-20261023" rel="noopener noreferrer"&gt;BlockVectra MCP server guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Why Decimal Can Round a UInt256 Token Amount, and an Exact Alternative</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:45:28 +0000</pubDate>
      <link>https://dev.to/blockvectra/why-decimal-can-round-a-uint256-token-amount-and-an-exact-alternative-10li</link>
      <guid>https://dev.to/blockvectra/why-decimal-can-round-a-uint256-token-amount-and-an-exact-alternative-10li</guid>
      <description>&lt;p&gt;The documented raw balance is a UInt256String: an unsigned integer string that can reach 78 digits. When you display one in Python, &lt;code&gt;Decimal&lt;/code&gt; looks like the obvious tool. The trap is subtle: building a &lt;code&gt;Decimal&lt;/code&gt; from a string is exact, but arithmetic on it can round.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the rounding happens
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Decimal(raw)&lt;/code&gt; keeps every digit, because construction ignores the context precision. Operations such as &lt;code&gt;scaleb&lt;/code&gt;, &lt;code&gt;normalize&lt;/code&gt;, multiplication, or division round their result to the context precision, which defaults to 28 significant digits. That holds even at scale 0, where you intend no change at all.&lt;/p&gt;

&lt;p&gt;The proof below takes the maximum UInt256 value and runs it through &lt;code&gt;scaleb(0).normalize()&lt;/code&gt; with precision set to 28 inside &lt;code&gt;localcontext()&lt;/code&gt;. The global context is not mutated. The result keeps the leading digits and replaces the rest with zeros, and nothing raises an error.&lt;/p&gt;

&lt;p&gt;This does not make &lt;code&gt;Decimal&lt;/code&gt; the wrong tool. Decimal calculations need a context appropriate to the inputs and operations. For this display-only task, integer formatting avoids depending on that context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exact integer formatting
&lt;/h2&gt;

&lt;p&gt;Python integers have arbitrary precision. &lt;code&gt;int(raw)&lt;/code&gt; parses the string exactly, and &lt;code&gt;divmod(value, 10 ** decimals)&lt;/code&gt; splits the whole and fractional parts with no rounding. The remainder is left-padded to &lt;code&gt;decimals&lt;/code&gt; digits, trailing zeros are stripped, and the parts are joined. No Decimal context is involved in this formatting step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;localcontext&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_display_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decimals&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;  &lt;span class="c1"&gt;# no decimals metadata: keep the raw integer
&lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# parse the decimal string exactly
&lt;/span&gt;    &lt;span class="n"&gt;whole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;divmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;whole&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fraction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;whole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;115792089237316195423570985008687907853269984665640564039457584007913129639935&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;localcontext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;
    &lt;span class="n"&gt;rounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;scaleb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;rounded&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;  &lt;span class="c1"&gt;# Arithmetic rounded an exact Decimal input.
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;to_display_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;to_display_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;  &lt;span class="c1"&gt;# Raw units; the scale is still unknown.
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;to_display_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;115792089237316195423570985008687907853269984665640564039457.584007913129639935&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rounded:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rounded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exact:  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;to_display_amount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS: 4 offline assertions; no network requests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Python 3.14.8 this exits with status 0, makes no network requests, and passes four assertions: the &lt;code&gt;Decimal&lt;/code&gt; path differs from the input, scale 0 returns the exact string, &lt;code&gt;None&lt;/code&gt; returns the raw string, and scale 18 yields the expected 18-digit fraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nullable scale
&lt;/h2&gt;

&lt;p&gt;The documented decimals value is an integer from 0 to 255, or null. When it is &lt;code&gt;None&lt;/code&gt;, the formatter returns the raw integer string unchanged. That output is in raw units. It is not a scaled amount, and the scale is still unknown, so label it as raw instead of assuming 18.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;The formatter assumes the input is already a valid unsigned integer string under the documented UInt256String format. It is a formatter, not a validator, so check input at your boundary first. The field definitions are in the &lt;a href="https://docs.blockvectra.com/en/guides/wallet-assets/?ref=dev-python-decimal-precision" rel="noopener noreferrer"&gt;wallet assets guide&lt;/a&gt;, and the standalone function is in this &lt;a href="https://gist.github.com/gjwang/8b1c854fe30f54ae271dcca6dc7c8867#file-exact-token-amount-py" rel="noopener noreferrer"&gt;gist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Resumable eth_getLogs scans: fixed chunks, saved checkpoints, safe restarts</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Wed, 21 Oct 2026 10:25:58 +0000</pubDate>
      <link>https://dev.to/blockvectra/resumable-ethgetlogs-scans-fixed-chunks-saved-checkpoints-safe-restarts-3b3p</link>
      <guid>https://dev.to/blockvectra/resumable-ethgetlogs-scans-fixed-chunks-saved-checkpoints-safe-restarts-3b3p</guid>
      <description>&lt;p&gt;A rate limit, malformed response, or process restart can interrupt a log scan after successful chunks. The useful question then is not "how do I re-run the query?" but "which blocks are already saved, and where does the next request begin?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The first response is not a saved checkpoint
&lt;/h2&gt;

&lt;p&gt;A returned &lt;code&gt;eth_getLogs&lt;/code&gt; result is only candidate data. The client advances to the next block only after two things happen in order: the HTTP status and JSON-RPC envelope pass validation (&lt;code&gt;jsonrpc&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, no &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;result&lt;/code&gt; is an array), and a new checkpoint containing that chunk has been committed to disk. A successful empty array is valid. If either validation or the commit fails, the run stops at that chunk. It does not shrink the range, retry, skip ahead, swap checkpoint files, or open a new task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a fixed window
&lt;/h2&gt;

&lt;p&gt;Before adding a fallback, list the calls your scanner actually makes: for example, &lt;code&gt;eth_blockNumber&lt;/code&gt;, &lt;code&gt;eth_getLogs&lt;/code&gt;, and any block or transaction lookups in your processor. Matching &lt;code&gt;eth_chainId&lt;/code&gt; alone doesn't confirm those methods, permitted block ranges, or historical coverage. Check public and keyed endpoint policies separately. &lt;a href="https://docs.blockvectra.com/en/guides/choose-rpc-provider/?ref=devto-getlogs-workflow-1022" rel="noopener noreferrer"&gt;RPC workflow checklist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use Node.js 24 or later. Get your API key from the &lt;a href="https://console.blockvectra.com/?ref=devto-getlogs-checkpoints-1021" rel="noopener noreferrer"&gt;BlockVectra console&lt;/a&gt;, then download the &lt;a href="https://docs.blockvectra.com/examples/getlogs-resumable.mjs?ref=devto-getlogs-checkpoints-1021" rel="noopener noreferrer"&gt;client example&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;Pick a chain, address, and inclusive &lt;code&gt;fromBlock&lt;/code&gt;/&lt;code&gt;toBlock&lt;/code&gt; that you have confirmed and that the node currently serves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Supply BLOCKVECTRA_API_KEY through your secret manager/environment.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CHAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;confirmed-chain-slug&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LOG_ADDRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;confirmed-address&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FROM_BLOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;confirmed-start-block&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TO_BLOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;confirmed-end-block&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CHECKPOINT_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"./getlogs-checkpoint.json"&lt;/span&gt;
&lt;span class="c"&gt;# optional: export CHUNK_BLOCKS=100&lt;/span&gt;
node getlogs-resumable.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace every placeholder with your own confirmed values. &lt;code&gt;BLOCKVECTRA_API_KEY&lt;/code&gt; is read from your own environment; the client does not create accounts. &lt;code&gt;CHUNK_BLOCKS&lt;/code&gt; is optional and must be a positive decimal integer. Each request spans &lt;code&gt;min(CHUNK_BLOCKS, max_logs_block_range)&lt;/code&gt; blocks, where the limit is read live from &lt;code&gt;GET /v1/chains&lt;/code&gt;; without &lt;code&gt;CHUNK_BLOCKS&lt;/code&gt;, the catalog value is used. Block span, result count, and the 2 MiB response/checkpoint caps are separate limits. The task allows 20 saved chunks, including restored ones. &lt;/p&gt;

&lt;h2&gt;
  
  
  Stop, then continue the same task
&lt;/h2&gt;

&lt;p&gt;Change nothing in the task identity: same chain, address, and complete window. Keep the same checkpoint file and run again. The client re-validates the checkpoint's task binding, contiguous chunk order, and &lt;code&gt;nextBlock&lt;/code&gt;, then requests only blocks not yet saved. You may change &lt;code&gt;CHUNK_BLOCKS&lt;/code&gt; on resume; it only affects how the remaining range is split and never re-fetches saved chunks.&lt;/p&gt;

&lt;p&gt;One process holds the checkpoint at a time through &lt;code&gt;CHECKPOINT_FILE.lock&lt;/code&gt;; if a crash leaves the lock behind, confirm that no process owns it, then remove it manually. Checkpoints are written through a &lt;code&gt;0600&lt;/code&gt; temporary file, &lt;code&gt;fsync&lt;/code&gt;, and an atomic rename. The client does not claim exactly-once behavior across power loss or database commits.&lt;/p&gt;

&lt;p&gt;A final &lt;code&gt;complete&lt;/code&gt; status means the client read every block in that fixed window successfully. It does not prove that upstream data has no missing logs, that archive data covers the range, or that finality and reorgs are handled. Persist idempotently and verify chain hashes on the consumer side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check one block against its receipts
&lt;/h2&gt;

&lt;p&gt;A saved checkpoint tells you where a scan stopped. It does not tell you whether an RPC returned every log. For a suspicious block, the new &lt;a href="https://docs.blockvectra.com/examples/getlogs-reconcile-block.mjs" rel="noopener noreferrer"&gt;offline checker&lt;/a&gt; compares the complete transaction-hash list and one receipt per transaction with your saved &lt;code&gt;eth_getLogs&lt;/code&gt; result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node getlogs-reconcile-block.mjs block-material.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Node.js 24 or later; the script makes no network calls and does not modify checkpoints. The &lt;a href="https://docs.blockvectra.com/en/guides/getlogs-block-range/#reconcile-one-block-offline" rel="noopener noreferrer"&gt;guide&lt;/a&gt; describes the input format and optional address/topic filter. It returns &lt;code&gt;consistent&lt;/code&gt;, &lt;code&gt;inconsistent&lt;/code&gt; or &lt;code&gt;unresolved&lt;/code&gt;. Keep a range unresolved on either of the latter two results. Agreement between the supplied files does not establish canonicality, validate a receipts root or prove complete history.&lt;/p&gt;

&lt;p&gt;The full walkthrough, limits, and recovery rules are in the &lt;a href="https://docs.blockvectra.com/en/guides/getlogs-block-range/?ref=devto-getlogs-checkpoints-1021" rel="noopener noreferrer"&gt;resumable eth_getLogs guide&lt;/a&gt;. You can also download the &lt;a href="https://docs.blockvectra.com/examples/getlogs-resumable.mjs?ref=devto-getlogs-checkpoints-1021" rel="noopener noreferrer"&gt;public client example&lt;/a&gt; and check its &lt;a href="https://docs.blockvectra.com/examples/getlogs-client-cases.md?ref=devto-getlogs-checkpoints-1021" rel="noopener noreferrer"&gt;35 synthetic response fixtures&lt;/a&gt; with a mock transport.&lt;/p&gt;

&lt;p&gt;For two-direction wallet scans, the &lt;a href="https://github.com/unibitorg/evm-log-backfill-checkpoints" rel="noopener noreferrer"&gt;six offline checkpoint cases&lt;/a&gt; cover joint progress, persistence failures and gaps between parallel pages. These synthetic cases use no live RPC and are separate from the client's 35 response fixtures.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a cancelled writer finishes late
&lt;/h2&gt;

&lt;p&gt;Cancellation can leave an old write in flight. In a small synthetic example, writer B saves watermark 120; cancelled writer A resumes and overwrites it with 110. Checking ownership before an &lt;code&gt;await&lt;/code&gt; still allows this race.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/unibitorg/evm-log-backfill-checkpoints/tree/b8abfee7ba1d6e8de299d98d4bfbac704b33fabf" rel="noopener noreferrer"&gt;Four runnable writer-fencing cases&lt;/a&gt; compare cancellation alone, an early ownership check, a synchronous commit-time fence, and a current-writer control. Clone or download that revision, then run with Node.js 22 or later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--test&lt;/span&gt; writer-fencing.test.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model compares the writer's generation and replaces the cache without an &lt;code&gt;await&lt;/code&gt; between them. It uses an in-memory cache; database transactions, crash recovery and reorg handling need their own checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When &lt;code&gt;eth_getLogs&lt;/code&gt; returns an empty array
&lt;/h2&gt;

&lt;p&gt;An empty response is not, by itself, a completeness check. If you have a transaction receipt with the event you expected, compare it with an address-filtered log request for that exact block. Check the chain and block hash as well as the address, topics, data and event position before widening the range.&lt;/p&gt;

&lt;p&gt;Here is a &lt;a href="https://gist.github.com/gjwang/5a8f80f5e441a5701e2e01ae73f187bf" rel="noopener noreferrer"&gt;downloadable BSC observation with raw responses and an offline checker&lt;/a&gt;. Download all files into one directory, then use Node.js 22+:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node verify-sample.mjs observed-sample.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The saved check returns &lt;code&gt;PASS 1 receipt event matched among 4 returned logs&lt;/code&gt;. It compares the selected receipt event across eight fields. The original RPC request filtered by address and one fixed block; event selection happens offline. No key or network is needed to run the checker.&lt;/p&gt;

&lt;p&gt;These are three historical QA requests from 2026-10-25, not a rerun of the reporter's failing provider. They show consistency for one saved event, not complete history or the cause of &lt;a href="https://github.com/bnb-chain/bsc/issues/3853" rel="noopener noreferrer"&gt;the reported empty-result issue&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>node</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Gasless transactions: separate the signer, gas payer, and token owner</title>
      <dc:creator>BlockVectra</dc:creator>
      <pubDate>Wed, 21 Oct 2026 01:28:57 +0000</pubDate>
      <link>https://dev.to/blockvectra/gasless-transactions-separate-the-signer-gas-payer-and-token-owner-1elp</link>
      <guid>https://dev.to/blockvectra/gasless-transactions-separate-the-signer-gas-payer-and-token-owner-1elp</guid>
      <description>&lt;p&gt;&lt;em&gt;AI-generated engineering note from BlockVectra.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Gasless" describes an experience, not a protocol. Before writing any test, name four roles. In a demo they are often one address; in production they usually are not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signer&lt;/strong&gt;: the key that authorizes an action off-chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas payer&lt;/strong&gt;: in the direct EOA-relaying model used for this discussion, the transaction-sending EOA that pays native gas. Other sponsorship architectures can separate these roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token owner&lt;/strong&gt;: the account whose tokens are spent or whose allowance is authorized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract caller&lt;/strong&gt;: the address the target contract sees as &lt;code&gt;msg.sender&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many bugs come from treating two of these as one. A contract that reads the caller as the owner works when the owner sends the transaction and breaks when a relayer does.&lt;/p&gt;

&lt;p&gt;Two token authorization patterns are useful to distinguish. A signed approval, as in &lt;a href="https://eips.ethereum.org/EIPS/eip-2612" rel="noopener noreferrer"&gt;ERC-2612&lt;/a&gt;, only changes an allowance; a transfer must still follow. A signed transfer, as in &lt;a href="https://eips.ethereum.org/EIPS/eip-3009" rel="noopener noreferrer"&gt;ERC-3009&lt;/a&gt;, binds sender, recipient, amount, validity window and nonce into one authorization that a third party can submit. Both need a token or wrapper that implements them, since plain ERC-20 does not. The official ERC-3009 page currently marks it Draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assert roles, not just outcomes
&lt;/h2&gt;

&lt;p&gt;A passing "transfer succeeded" check hides which account paid for what. Assert each role's balance and allowance separately, before and after, and use distinct token-owner/signer, relayer, and recipient addresses so a mix-up cannot pass by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proposed test matrix
&lt;/h2&gt;

&lt;p&gt;This is a suggested plan, not a record of executed results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Assert&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zero-native user&lt;/td&gt;
&lt;td&gt;Owner holds tokens and no native coin; relayer pays gas&lt;/td&gt;
&lt;td&gt;Call succeeds; owner token balance drops by the exact amount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-token sponsor&lt;/td&gt;
&lt;td&gt;Relayer holds gas funds but zero tokens&lt;/td&gt;
&lt;td&gt;Relayer token balance is zero before and after&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actor binding&lt;/td&gt;
&lt;td&gt;Token-owner/signer, relayer, and recipient are distinct&lt;/td&gt;
&lt;td&gt;A signed transfer debits the authorized owner and credits the authorized recipient; a permit updates the authorized owner-to-spender allowance without transferring tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization lifecycle&lt;/td&gt;
&lt;td&gt;Valid, expired, then replayed signature&lt;/td&gt;
&lt;td&gt;Valid works once; the others revert and leave state unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vault accounting&lt;/td&gt;
&lt;td&gt;Tokens credited directly to a vault&lt;/td&gt;
&lt;td&gt;Deposit counters match what the design says they should mean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  One public case
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/bytesbrains/16ane/blob/1856b9f3307e530b55fff4eefa3f9bc903feeffe/test/AneAgentVaultSignedActions.t.sol#L226-L270" rel="noopener noreferrer"&gt;16ane repository test, pinned to one commit&lt;/a&gt;, has a relayer holding zero mock tANE submit a signed ERC-3009 transfer. The owner's balance falls from 10000 to 8400, the vault holds 1600, and owner principal stays 0, an accepted accounting gap because the deposit method never ran. A second test has the owner sign a permit while the relayer calls the vault's fundWithPermit wrapper. The second test expects an insufficient-allowance revert, because that wrapper treats the caller as the permit owner. That limits the wrapper, not ERC-2612 relaying in general. The tests do not assert that the owner holds zero native coin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying for real
&lt;/h2&gt;

&lt;p&gt;Read &lt;code&gt;balanceOf&lt;/code&gt; and &lt;code&gt;allowance&lt;/code&gt; for the caller and the owner separately, and compare receipts and state deltas, one branch at a time. A simulation or a log status is not end-to-end proof. Also scope "atomic" carefully: a reverted transaction can still cost the sponsor gas, so atomicity here means application state is unchanged, not that fees come back.&lt;/p&gt;

&lt;p&gt;If you are wiring agents to onchain actions, the &lt;a href="https://docs.blockvectra.com/en/guides/ai-agents/?ref=devto-gasless-roles-1021" rel="noopener noreferrer"&gt;BlockVectra agent integration documentation&lt;/a&gt; covers the integration side.&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>programming</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
