ผมเสนอป้าย "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 ทำให้ผมเห็นว่า แผนเดิมจริง ๆ แล้วเป็นการโกหกของโปรดักต์
ความคิดแรกของผมเป็นแบบนี้:
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 และเวลามีความสำคัญหมายเลขบล็อกคือจุดยึดเชิงวัตถุประสงค์เพียงจุดเดียว — เป็นความจริงบนเชน ไม่ถูกบิดเบือนโดยกลยุทธ์แคชของเรา
ที่ขึ้นโปรดักชัน:
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 ดีกว่าที่จะใส่แท็กว่า "อาจเปลี่ยน" เกินไป มากกว่าจะแสดงค่าที่เปลี่ยนได้แบบสแตติกแบบเงียบ ๆ
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 มาปีกว่า สิ่งที่ผมตกผลึกได้สูงสุดไม่ใช่ความเร็วในการเขียนโค้ด — แต่คือ มันจะโต้แย้งแผนของผมเองอย่างจริงจัง ถ้าผมยอมหยุดและถาม
ตอนผมเสนอ "เพิ่มเลเบล 23s ago" ผมเองไม่เห็นว่ามีอะไรผิด ถ้าผมรีวิว PR ของตัวเอง ผมก็คงจะ merge เพราะมันดู "ถูกต้องอย่างชัดแจ้ง" เกินไป
แต่พอเปลี่ยนน้ำเสียงไปถาม Claude ว่า "23s นี่ลบอะไรกับอะไรแน่ และตรงกับช่วง latency ไหน?" มันก็คลี่ห่วงโซ่เหตุปัจจัยออกมา และทำให้เห็นว่าความหมายเชิงฟิสิกส์ของตัวเลขนั้นผิด
ผลลัพธ์ตรงนี้ไม่ใช่ "Claude เขียนโค้ดให้ผมเพิ่ม" แต่คือ "Claude กันคำโกหกของโปรดักต์ไม่ให้ขึ้นโปรดักชัน"
ถ้า 23s ago ขึ้นจริง คงไม่มีใครบ่น — มันดูสมเหตุสมผล และคนที่จะจับบักก็ขี้เกียจเตือน แต่ในสายตาคนที่รู้จริงในวงการนี้ เว็บ DeFi docs ของคุณจะมีรู: เว็บนี้ไม่รู้ว่าตัวเองกำลังแสดงอะไร
ครั้งหน้าเวลาตัดสินใจเรื่องโปรดักต์กับ Claude เผื่อเวลาไว้ 5 นาทีสำหรับขั้นตอนนี้:
"จากแผนที่ผมเพิ่งเสนอ ลองยกตัวอย่างสามวิธีที่มันอาจผิดได้ พร้อมสถานการณ์รูปธรรมแต่ละวิธี"
ถ้า Claude เจอวิธีที่คุณยังไม่ได้คิด 5 นาทีนั้นคุ้มค่าทันที