Free

איך Claude פירק את התוכנית "23s ago" שלי עצמי

הצעתי תווית "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 עזר לי להבין שהתוכנית המקורית היא בעצם שקר של מוצר.


אינטואיציה ראשונה: פשוט להדביק "23s ago"

המחשבה הראשונה שלי הייתה:

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 וגם זמן חשובים.

התיקון: block + age זה לצד זה, מחולק לפי סוג שדה

מספר הבלוק הוא העוגן האובייקטיבי היחיד — זאת האמת ב-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 לטעון נגדך

אחרי יותר משנה של עבודה עם Claude, המסקנה הגדולה שלי היא לא המהירות שבה הוא כותב קוד — אלא שהוא יטען ברצינות נגד התוכנית שלי, אם רק אטרח לעצור ולשאול.

כשהצעתי "הוסף label 23s ago", אני בעצמי לא ראיתי שום בעיה. אם הייתי עושה review ל-PR שלי בעצמי, סביר שהייתי ממזג — נראה יותר מדי "ברור שנכון".

אבל כששיניתי את הטון ושאלתי את Claude "ה-23s הזה מחסיר מה ממה בדיוק, ולאיזה מקטע latency הוא מתאים?", הוא פרס את שרשרת הסיבתיות והראה לי שהמשמעות הפיזית של המספר שגויה.

הרווח כאן הוא לא "Claude כתב לי עוד קוד". הוא "Claude מנע משקר של מוצר להגיע לפרודקשן".

אם 23s ago באמת היה עולה, אף אחד לא היה מתלונן — נראה סביר, ואלה שהיו מבחינים בבאג לא יטרחו להתריע. אבל בעיני מי שבאמת יודע את התחום, באתר ה-docs ל-DeFi שלך הייתה חור: האתר הזה לא יודע מה הוא מציג.

בפעם הבאה שאתה מקבל החלטת מוצר עם Claude, הקדש 5 דקות לשלב הזה:

"לתוכנית שהרגע הצעתי, מנה שלוש דרכים שבהן היא יכולה להיות שגויה. לכל אחת תן תרחיש קונקרטי."

אם Claude ימצא אחת שלא חשבת עליה, חמש הדקות האלה כבר השתלמו.