Free

Claude'a Kendi Önerdiğim "23s ago" Planını Çürüttürdüm

On-chain veriye "23s ago" etiketi önerdim. Claude bunun bir ürün yalanı olduğunu gösterdi.


smarts.md, akıllı sözleşmeler için bir live docs sitesi. Her sözleşme sayfası, doğrudan zincirden okunan değerleri gösteriyor — Uniswap pool likiditesi, ERC-20 totalSupply, özel sözleşmenin owner alanı. Bu değerler en fazla 60 saniye eski olabiliyor (Solid Cache TTL = 60s).

Ama sayfada "bu ne zaman okundu?" diye hiçbir ipucu yoktu. Mintlify gibi görünüyordu — statik bir dokümandan ayırt edilemezdi.

Veri yenileme stratejisini Claude ile birlikte denetlerken altı boşluk listeledim. Boşluk 4 tek satırdı:

canlı veride tazelik göstergesi yok — bir "as of block X / Y seconds ago" etiketi ekle.

To-do'yu yazıp yemek yemeye gittim. Dönünce o tek satıra bir saat takıldım. Sonunda yayına aldığım yön ilk içgüdümden tamamen farklıydı — ve Claude sayesinde fark ettim ki orijinal plan aslında bir ürün yalanıydı.


İlk içgüdü: doğrudan "23s ago" yapıştır

İlk düşüncem şuydu:

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

Basit, doğrudan, kullanıcı anında anlar.

Yemekten sonra tekrar baktım ve Claude'a birkaç soru sormaya başladım:

Bu "23s" tam olarak neyden neyi çıkarıyor?

Açıkça yazdı: now - cache.fetched_at. Yani değeri cache'e yazdığımızdan bu yana geçen süre.

Peki zincirin başına göre gerçekten ne kadar geride kaldık?

Claude düşündü: Multicall'ın tüm view fonksiyonlarını tek seferde okuyup cache'e yazıyor, cache 60s boyunca geçerli. Ama yazma anında okuduğumuz block'un kendisi zaten geçmişte — Ethereum'un blok süresinin alt sınırı 0–12s.

Gerçek nedensel zincir şu:

zincir başı yeni bir block üretir
  └→ RPC bizim tarafımızda block N'i okur (head'den 0–12s geride)
       └→ Solid Cache'e yazılır
            └→ kullanıcının ekranında render (cache 60s'ye kadar)

O zaman "23s ago" etiketindeki 23s hangi segmente karşılık geliyor?

Sadece son segmente: now - fetched_at, cache yazımından şimdiye kadar geçen süre.

Zincir tarafındaki gecikme hiçbir şekilde yansıtılmıyor.

Somut örnek: kullanıcı "fetched 5s ago" görüyor, ama o block kendisi zaten 12s yaşında ve zincir başı 2 block daha üretmiş olabilir — gerçek zincir gecikmesi ≈ 24s, etiket 5s diyor.

İşte o anda anladım: "tazeliği" tek bir sayıya sıkıştırmak fiziksel olarak yanlış.

Üç bağımsız boyut

Claude bunu net yazmama yardım etti — üç bağımsız boyut var:

Boyut Ne ölçüyor Fiziksel taban
Block tazeliği zincir başı → okuduğumuz block Ethereum blok süresi 0–12s
Okuma tazeliği RPC okuma → cache yazma 0 (senkron)
Görüntüleme tazeliği cache yazma → kullanıcı ekranı 0–60s (cache TTL)

Bu üçünü tek bir "freshness: 35s" sayısına toplamak fiziksel olarak yok. chain → cache → screen zinciri senkron değil: cache yazıldığı anda donar, zincir ise blok üretmeye devam eder. Bu bir hassasiyet sorunu değil, kategori hatası.

Daha da önemlisi: hangi segmentin önemli olduğu, alanın kendisine bağlı.

  • decimals: kurucuda tanımlı, asla değişmez. Tazeliğe hiç gerek yok.
  • owner: yalnızca governance ile değişir, yıllarca yerinde durabilir. Hangi block'ta okuduğunu bilmen gerek (yoksa "beş upgrade öncesini" izliyor olursun), ama "23 saniye önce" gerekmiyor.
  • liquidity / slot0: her block'ta değişebilir. Hem block hem zaman önemli.

Çözüm: block + age yan yana, alan tipine göre ayrılmış

Block numarası tek nesnel çapa — bu zincirdeki gerçek, cache stratejimizden etkilenmez.

Yayına alınan görünüm:

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

Block, anlayan kullanıcı için; "23s ago", anlamayan için. İkisi de yalan söylemiyor: block fiziksel bir gerçek, "23s ago" açıkça cache yazımından bu yana geçen süre, "zincir başına göre gecikme" değil.

Alan özelliğine göre farklılaştırılmış görüntüleme:

# 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 kuralları:

Sınıf Örnek Görüntüleme
immutable decimals, name, symbol tazelik gösterilmez (saf gürültü olur)
slow owner, paused yalnızca block, saniye yok
fast slot0, liquidity, totalSupply block + saniye

Bu beyaz liste bilinçli olarak muhafazakâr — listede olmayan her şey fast. Bir değeri yanlışlıkla "canlı" olarak işaretlemek, değişebilen bir değeri sessizce statik göstermekten iyidir.

Uygulama: block numarası neredeyse bedava

Multicall3'ün kendisinde getBlockNumber() fonksiyonu var. Her batch'in sonuna ekle — tek RPC, fazladan bir değer, sıfıra yakın gecikme:

# 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 tek seferde
  Batch.new(block_number: block_number, results: results)
end

ViewCaller cache payload'u da sürüm aldı (CACHE_VERSION = "v2"), {fn_sig => result} yapısından Snapshot(results:, block_number:, fetched_at:) yapısına geçti. Snapshot, eski hash arabirimini korumak için def [](key) = @results[key] ve benzeri delege metotları kullanıyor — hiçbir caller'ın değişmesine gerek kalmadı.

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

Toplam PR: 24 dosya, +676 / -55.

Asıl ürün hikâyesi

Yayına çıktıktan sonra farklılaştırma cümlesini yeniden yazdım:

Smarts, alan bazında, durumun hangi block'ta okunduğunu açıkça söyleyen ilk DeFi dokümantasyonu.

"Mintlify'dan daha taze" demekten daha somut ve daha güvenilir. Gerçek gecikmeyi gizlemek yerine ortaya seriyor — sürekli geri döndüğüm CLAUDE.md ilkeleriyle uyumlu: "AI bir araç, vitrin değil" + "Build in Public".

Ders: Claude'a sizin karşınızda tartıştırın

Claude ile bir yılı aşkın süredir çalıştıktan sonra en büyük çıkarımım kod yazma hızı değil — durup sormaya tenezzül edersem kendi planımı ciddi şekilde çürütecek olması.

"23s ago etiketi ekle" diye önerdiğimde kendim hiçbir şeyin yanlış olduğunu görmüyordum. Kendi PR'ımı kendim incelesem, büyük ihtimalle merge ederdim — fazlasıyla "açıkça doğru" görünüyor.

Ama tonu değiştirip Claude'a "bu 23s tam olarak neyi neyden çıkarıyor ve hangi gecikme segmentine karşılık geliyor?" diye sorduğumda, nedensel zinciri açtı ve sayının fiziksel anlamının yanlış olduğunu gösterdi.

Buradaki kazanım "Claude bana fazladan kod yazdı" değil, "Claude bir ürün yalanının canlıya çıkmasını engelledi".

23s ago gerçekten yayına alınsaydı kimse şikâyet etmezdi — makul görünüyor ve hatayı fark edebilecek kişiler de bunu uyarmaya zahmet etmez. Ama gerçekten konuyu bilen birinin gözünde, DeFi docs siteniz şu deliği taşır: bu site neyi gösterdiğini bilmiyor.

Bir dahaki sefere Claude ile bir ürün kararı verirken bu adıma 5 dakika ayırın:

"Az önce önerdiğim plan için, yanlış olabileceği üç yolu listele. Her biri için somut bir senaryo ver."

Claude düşünmediğin bir tane bulursa o 5 dakika kendini fazlasıyla geri ödedi demektir.