Free

ตอนที่ Claude หักล้างแผน "23s ago" ที่ผมเสนอเอง

ผมเสนอป้าย "23s ago" สำหรับข้อมูล on-chain แต่ Claude ชี้ให้เห็นว่ามันเป็นการโกหกของโปรดักต์


smarts.md คือเว็บ live docs สำหรับ smart contract แต่ละหน้าคอนแทรกต์จะแสดงค่าหลายค่าที่อ่านสด ๆ จาก on-chain — liquidity ของ Uniswap pool, totalSupply ของ ERC-20, owner ของคอนแทรกต์ที่กำหนดเอง ค่าเหล่านี้อาจเก่าได้ถึง 60 วินาที (Solid Cache TTL = 60s)

แต่หน้าเว็บไม่มีอะไรเลยที่บอกว่า "อ่านเมื่อไหร่" ดูเหมือน Mintlify — แทบแยกไม่ออกจากเอกสารแบบสแตติก

ตอนผมตรวจกลยุทธ์รีเฟรชข้อมูลกับ Claude ผมแจกแจงช่องโหว่ไว้ 6 จุด ช่องโหว่ที่ 4 มีบรรทัดเดียว:

ข้อมูล live ไม่มีตัวบ่งชี้ความสด — เพิ่มเลเบล "as of block X / Y seconds ago"

เขียน todo เสร็จแล้วผมไปกินข้าวเที่ยง พอกลับมาก็ติดอยู่กับบรรทัดนี้หนึ่งชั่วโมง สุดท้ายทิศทางที่ผมเอาขึ้นโปรดักชันต่างจากสัญชาตญาณแรกอย่างสิ้นเชิง — และ Claude ทำให้ผมเห็นว่า แผนเดิมจริง ๆ แล้วเป็นการโกหกของโปรดักต์


สัญชาตญาณแรก: แปะ "23s ago" ไปเลย

ความคิดแรกของผมเป็นแบบนี้:

liquidity: 1,234,567 USDC   ← 23s ago
totalSupply: 27.3B USDC     ← 12s ago

เรียบ ตรงไปตรงมา ผู้ใช้เข้าใจทันที

หลังกินข้าวเสร็จ ผมกลับมาดูใหม่และเริ่มถาม Claude ไปสองสามคำถาม:

"23s" นี่เอาอะไรลบกับอะไรกันแน่?

มันบอกชัด: now - cache.fetched_at คือ เวลานับจากตอนที่เราเขียนค่าลงแคช

แล้วจริง ๆ เราตามหัวเชนช้าไปเท่าไหร่?

Claude คิดสักครู่: Multicall ของคุณอ่านฟังก์ชัน view ทั้งหมดทีเดียว เขียนลงแคช แคชใช้ได้ 60s แต่ block ที่เราอ่าน ณ ตอนเขียนแคชนั้นเองก็เป็นอดีตอยู่แล้ว — Ethereum มี block time ขั้นต่ำ 0–12s

ห่วงโซ่เหตุปัจจัยที่แท้จริงคือ:

หัวเชนสร้าง block ใหม่
  └→ RPC ของเราอ่านที่ block N (ช้าหัวเชน 0–12s)
       └→ เขียนลง Solid Cache
            └→ render บนหน้าจอผู้ใช้ (แคชอยู่ได้ถึง 60s)

แล้ว "23s ago" ในเลเบลคือช่วงไหน?

เฉพาะช่วงสุดท้าย: now - fetched_at, เวลาตั้งแต่เขียนแคชจนถึงตอนนี้

ความช้าฝั่งเชนไม่ถูกสะท้อนเลย

ตัวอย่างจริง: ผู้ใช้เห็น "fetched 5s ago" แต่ตัว block นั้นเองก็อายุ 12s แล้ว และหัวเชนอาจสร้างเพิ่มอีก 2 block — ความช้าจริงเทียบกับเชน ≈ 24s แต่เลเบลบอก 5s

ตอนนั้นผมรู้สึกได้ว่า: ยุบ "ความสด" ลงเป็นตัวเลขเดียว ผิดในเชิงฟิสิกส์

สามมิติที่อิสระต่อกัน

Claude ช่วยผมเขียนให้ชัด — มันมีสามมิติ ที่อิสระต่อกัน:

มิติ วัดอะไร ขอบเขตล่างทางฟิสิกส์
ความสดของบล็อก หัวเชน → block ที่เราอ่าน block time ของ Ethereum 0–12s
ความสดของการอ่าน RPC อ่าน → cache เขียน 0 (ซิงโครนัส)
ความสดของการแสดงผล cache เขียน → หน้าจอผู้ใช้ 0–60s (TTL ของแคช)

การรวมสามอย่างนี้เป็น "freshness: 35s" เดียว ไม่มีอยู่จริงในเชิงฟิสิกส์ ห่วงโซ่ chain → cache → screen ไม่ซิงโครนัส: แคชจะถูกแช่แข็งทันทีเมื่อเขียน ขณะที่เชนยังคงสร้าง block ต่อไป นี่ไม่ใช่ปัญหาความแม่นยำ แต่เป็นความผิดพลาดเชิงหมวดหมู่ (category error)

ที่สำคัญกว่านั้น: ช่วงไหนสำคัญขึ้นอยู่กับฟิลด์เอง

  • decimals: กำหนดใน constructor ไม่เปลี่ยนเลย ไม่ต้องการ ความสดเลย
  • owner: เปลี่ยนผ่าน governance เท่านั้น อาจอยู่นิ่งหลายปี ต้องรู้ว่าอ่านที่ block ไหน (เพื่อไม่ดูค่า "ก่อนอัปเกรดห้าครั้ง") แต่ไม่ต้องการ "23 วินาทีที่แล้ว"
  • liquidity / slot0: เปลี่ยนได้ทุก block ทั้ง block และเวลามีความสำคัญ

ทางแก้: block + age คู่กัน แยกตามประเภทฟิลด์

หมายเลขบล็อกคือจุดยึดเชิงวัตถุประสงค์เพียงจุดเดียว — เป็นความจริงบนเชน ไม่ถูกบิดเบือนโดยกลยุทธ์แคชของเรา

ที่ขึ้นโปรดักชัน:

liquidity: 1,234,567 USDC
↳ as of Block #19,234,567 · 23s ago

block สำหรับคนที่เข้าใจ "23s ago" สำหรับคนที่ไม่เข้าใจ ทั้งสองตัวไม่โกหก: block เป็นข้อเท็จจริงทางฟิสิกส์ "23s ago" หมายความตรง ๆ ว่า เวลาตั้งแต่เขียนแคช ไม่ใช่ "ช้าจากหัวเชน"

แสดงผลแยกตามคุณสมบัติของฟิลด์:

# app/services/chain_reader/field_mutability.rb
module ChainReader
  module FieldMutability
    IMMUTABLE = %w[
      name symbol decimals
      DOMAIN_SEPARATOR PERMIT_TYPEHASH
      factory token0 token1 fee tickSpacing
      asset underlying
    ].freeze

    SLOW = %w[
      owner
      paused deprecated
      pauser blacklister masterMinter rescuer
      upgradedAddress implementation
      maxTotalSupply
    ].freeze

    def self.classify(function_name)
      return :immutable if IMMUTABLE.include?(function_name)
      return :slow      if SLOW.include?(function_name)
      :fast
    end
  end
end

กฎการ render:

คลาส ตัวอย่าง แสดงผล
immutable decimals, name, symbol ไม่แสดงความสด (เป็นแค่สัญญาณรบกวน)
slow owner, paused แสดง block อย่างเดียว ไม่มีวินาที
fast slot0, liquidity, totalSupply block + วินาที

whitelist นี้ออกแบบไว้แบบอนุรักษ์นิยมโดยตั้งใจ — อะไรที่ไม่อยู่ในรายการก็ตกเป็น fast ดีกว่าที่จะใส่แท็กว่า "อาจเปลี่ยน" เกินไป มากกว่าจะแสดงค่าที่เปลี่ยนได้แบบสแตติกแบบเงียบ ๆ

รายละเอียดการ implement: หมายเลข block แทบไม่มีต้นทุน

Multicall3 มีฟังก์ชัน getBlockNumber() ของตัวเอง เติมไว้ท้าย batch แต่ละครั้ง — RPC ครั้งเดียว เพิ่มค่าเดียว latency แทบเป็นศูนย์:

# app/services/chain_reader/multicall3_client.rb
BLOCK_NUMBER_FN = {
  "name" => "getBlockNumber",
  "inputs" => [],
  "outputs" => [{ "type" => "uint256" }]
}.freeze

def call(calls)
  block_call = Call.new(target: ADDRESS, function: BLOCK_NUMBER_FN)
  augmented = calls + [block_call]
  # ...aggregate3 ในทีเดียว
  Batch.new(block_number: block_number, results: results)
end

payload ที่แคชของ ViewCaller ก็ bump version ด้วย (CACHE_VERSION = "v2") จาก {fn_sig => result} กลายเป็น Snapshot(results:, block_number:, fetched_at:) Snapshot delegate def [](key) = @results[key] และเมธอดอื่น ๆ เพื่อรักษาอินเทอร์เฟซ hash เดิมไว้ — caller เดิมไม่ต้องแก้

UI helper:

def freshness_tag_for(fn)
  return nil unless @live_snapshot&.block_number

  mutability = ChainReader::FieldMutability.classify(fn["name"])
  return nil if mutability == :immutable

  block_text = "Block ##{number_with_delimiter(@live_snapshot.block_number)}"
  label =
    if mutability == :fast && @live_snapshot.fetched_at
      "as of #{block_text} · #{freshness_phrase(@live_snapshot.fetched_at)}"
    else
      "as of #{block_text}"
    end
  # ...
end

PR ทั้งหมด: 24 ไฟล์, +676 / -55

เรื่องราวจริงของโปรดักต์

หลังขึ้นโปรดักชัน ผมเขียนจุดต่างใหม่:

Smarts คือ DeFi docs แห่งแรกที่ บอกคุณตามแต่ละฟิลด์ ว่าสถานะของมันถูกอ่านที่ block ไหน

เป็นรูปธรรมและน่าเชื่อกว่า "เราสดกว่า Mintlify" มัน เปิดเผย ความช้าจริงแทนที่จะซ่อน — สอดคล้องกับสองหลักการในไฟล์ CLAUDE.md ของผมที่ผมยึดตลอด: "AI เป็นเครื่องมือ ไม่ใช่ของโชว์" + "Build in Public"

บทเรียน: ให้ Claude เถียงกับคุณ

หลังทำงานกับ Claude มาปีกว่า สิ่งที่ผมตกผลึกได้สูงสุดไม่ใช่ความเร็วในการเขียนโค้ด — แต่คือ มันจะโต้แย้งแผนของผมเองอย่างจริงจัง ถ้าผมยอมหยุดและถาม

ตอนผมเสนอ "เพิ่มเลเบล 23s ago" ผมเองไม่เห็นว่ามีอะไรผิด ถ้าผมรีวิว PR ของตัวเอง ผมก็คงจะ merge เพราะมันดู "ถูกต้องอย่างชัดแจ้ง" เกินไป

แต่พอเปลี่ยนน้ำเสียงไปถาม Claude ว่า "23s นี่ลบอะไรกับอะไรแน่ และตรงกับช่วง latency ไหน?" มันก็คลี่ห่วงโซ่เหตุปัจจัยออกมา และทำให้เห็นว่าความหมายเชิงฟิสิกส์ของตัวเลขนั้นผิด

ผลลัพธ์ตรงนี้ไม่ใช่ "Claude เขียนโค้ดให้ผมเพิ่ม" แต่คือ "Claude กันคำโกหกของโปรดักต์ไม่ให้ขึ้นโปรดักชัน"

ถ้า 23s ago ขึ้นจริง คงไม่มีใครบ่น — มันดูสมเหตุสมผล และคนที่จะจับบักก็ขี้เกียจเตือน แต่ในสายตาคนที่รู้จริงในวงการนี้ เว็บ DeFi docs ของคุณจะมีรู: เว็บนี้ไม่รู้ว่าตัวเองกำลังแสดงอะไร

ครั้งหน้าเวลาตัดสินใจเรื่องโปรดักต์กับ Claude เผื่อเวลาไว้ 5 นาทีสำหรับขั้นตอนนี้:

"จากแผนที่ผมเพิ่งเสนอ ลองยกตัวอย่างสามวิธีที่มันอาจผิดได้ พร้อมสถานการณ์รูปธรรมแต่ละวิธี"

ถ้า Claude เจอวิธีที่คุณยังไม่ได้คิด 5 นาทีนั้นคุ้มค่าทันที