הצעתי תווית "23s ago" לנתוני on-chain. Claude הראה לי שזו שקר של המוצר.
smarts.md הוא אתר live docs לחוזים חכמים. כל עמוד חוזה מציג סדרת ערכים שנקראים ישירות מה-on-chain — liquidity של pool ב-Uniswap, totalSupply של ERC-20, owner של חוזה כלשהו. הערכים האלה יכולים להיות ישנים עד 60 שניות (Solid Cache TTL = 60s).
אבל בדף לא היה שום סימן ל"מתי זה נקרא?". זה נראה כמו Mintlify — לא ניתן להבחין מ-doc סטטי.
תוך כדי audit של אסטרטגיית רענון הנתונים יחד עם Claude, פירטתי שש פערים. פער 4 היה בשורה אחת:
אין אינדיקטור טריות לנתוני live — להוסיף label "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. כלומר הזמן מאז שכתבנו את הערך ל-cache.
אז כמה אנחנו באמת פיגור אחרי ראש השרשרת?
Claude חשב: ה-Multicall שלך קורא את כל פונקציות ה-view בבת אחת, כותב ל-cache, וה-cache תקף ל-60s. אבל ה-block שאנחנו קוראים בו ברגע הכתיבה כבר עצמו בעבר — ל-Ethereum יש רצפה של 0–12s לבלוק.
שרשרת הסיבתיות האמיתית:
ראש השרשרת מייצר block חדש
└→ ה-RPC שלנו קורא ב-block N (פיגור 0–12s אחרי הראש)
└→ אנחנו כותבים ל-Solid Cache
└→ render על מסך המשתמש (cache עד 60s)
אז ה-"23s ago" שב-label מתאים לאיזה מקטע?
רק למקטע האחרון: now - fetched_at, הזמן מכתיבת ה-cache עד עכשיו.
הפיגור מצד השרשרת לא משתקף בכלל.
דוגמה קונקרטית: המשתמש רואה "fetched 5s ago", אבל ה-block עצמו כבר בן 12s, וראש השרשרת אולי ייצר עוד 2 בלוקים — פיגור אמיתי מהשרשרת ≈ 24s, ה-label אומר 5s.
ברגע הזה הבנתי: לדחוס "טריות" למספר אחד הוא שגוי פיזית.
Claude עזר לי לכתוב את זה בבירור — יש שלושה ממדים בלתי תלויים:
| ממד | מה הוא מודד | רצפה פיזית |
|---|---|---|
| טריות בלוק | ראש השרשרת → ה-block שקראנו | block time של Ethereum 0–12s |
| טריות קריאה | RPC קורא → cache כותב | 0 (סינכרוני) |
| טריות תצוגה | cache כותב → מסך המשתמש | 0–60s (TTL של cache) |
לסכום את שלושת אלה ל-"freshness: 35s" יחיד לא קיים פיזית. השרשרת chain → cache → screen לא סינכרונית: ה-cache קופא ברגע הכתיבה, בעוד השרשרת ממשיכה לייצר בלוקים. זה לא בעיית דיוק, זו טעות קטגוריה.
חשוב יותר: איזה מקטע חשוב — תלוי בשדה עצמו.
decimals: מוגדר ב-constructor, לא משתנה לעולם. לא צריך טריות בכלל.owner: משתנה רק דרך governance, יכול להישאר במקום שנים. צריך לדעת באיזה block נקרא (כדי לא לראות ערך "מלפני חמישה upgrade-ים"), אבל לא "לפני 23 שניות".liquidity / slot0: עשויים להשתנות בכל block. גם block וגם זמן חשובים.מספר הבלוק הוא העוגן האובייקטיבי היחיד — זאת האמת ב-on-chain, לא מעוותת על ידי אסטרטגיית ה-cache שלנו.
מה שעלה:
liquidity: 1,234,567 USDC
↳ as of Block #19,234,567 · 23s ago
block למי שמבין, "23s ago" למי שלא. אף אחד מהשניים לא משקר: block הוא עובדה פיזית, "23s ago" אומר במפורש זמן מאז כתיבת ה-cache, לא "פיגור אחרי ראש השרשרת".
תצוגה מובחנת לפי תכונת השדה:
# 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 גרסה (CACHE_VERSION = "v2"), מ-{fn_sig => result} ל-Snapshot(results:, block_number:, fetched_at:). ה-Snapshot מאציל def [](key) = @results[key] ודומיהם כדי לשמור על ממשק ה-hash הישן — אף caller לא היה צריך להשתנות.
ה-helper של ה-UI:
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 הראשון שאומר לך, לכל שדה, באיזה block נקרא המצב שלו.
יותר קונקרטי ויותר אמין מ"אנחנו טריים יותר מ-Mintlify". זה חושף את ה-latency האמיתי במקום להסתיר אותו — בקו אחד עם שני העקרונות מ-CLAUDE.md שלי שאני חוזר אליהם: "AI הוא כלי, לא תצוגה" + "Build in Public".
אחרי יותר משנה של עבודה עם Claude, המסקנה הגדולה שלי היא לא המהירות שבה הוא כותב קוד — אלא שהוא יטען ברצינות נגד התוכנית שלי, אם רק אטרח לעצור ולשאול.
כשהצעתי "הוסף label 23s ago", אני בעצמי לא ראיתי שום בעיה. אם הייתי עושה review ל-PR שלי בעצמי, סביר שהייתי ממזג — נראה יותר מדי "ברור שנכון".
אבל כששיניתי את הטון ושאלתי את Claude "ה-23s הזה מחסיר מה ממה בדיוק, ולאיזה מקטע latency הוא מתאים?", הוא פרס את שרשרת הסיבתיות והראה לי שהמשמעות הפיזית של המספר שגויה.
הרווח כאן הוא לא "Claude כתב לי עוד קוד". הוא "Claude מנע משקר של מוצר להגיע לפרודקשן".
אם 23s ago באמת היה עולה, אף אחד לא היה מתלונן — נראה סביר, ואלה שהיו מבחינים בבאג לא יטרחו להתריע. אבל בעיני מי שבאמת יודע את התחום, באתר ה-docs ל-DeFi שלך הייתה חור: האתר הזה לא יודע מה הוא מציג.
בפעם הבאה שאתה מקבל החלטת מוצר עם Claude, הקדש 5 דקות לשלב הזה:
"לתוכנית שהרגע הצעתי, מנה שלוש דרכים שבהן היא יכולה להיות שגויה. לכל אחת תן תרחיש קונקרטי."
אם Claude ימצא אחת שלא חשבת עליה, חמש הדקות האלה כבר השתלמו.