Free

לגרום ל-Claude להעביר מימוש x402 שנכתב ידנית אל ה-gem הקהילתי

מיגרציה מכתוב-יד → gem: נטו -622/+317 שורות. הבקר יורד מ-30 שורות אינסטלציה פרוטוקולית ל-4. מלכודות: importmap מוריד pin בשקט, YAML קורא 0x... כמספר שלם.


Diff של commit אחד:

19 files changed, 317 insertions(+), 622 deletions(-)

נמחק:

app/services/x402/facilitator_client.rb        53 שורות
app/services/x402/payment_handler.rb           86 שורות
test/services/x402/facilitator_client_test.rb  112 שורות
test/services/x402/payment_handler_test.rb     108 שורות

נוסף: שורה אחת ב-Gemfile, config/initializers/x402.rb (29 שורות), שתי מתודות record_x402! על Purchase/Subscription + בדיקות model מקבילות.

זה לא refactor — זו החלפה של החלק שכתבתי בחלק שמישהו אחר כתב. גרסת היד עבדה שבועיים. תשלומים חד-פעמיים, מנויים, תיעוד tx_hash — הכל עבד. אז למה להעביר?

הפוסט הזה מדבר על איך לגרום ל-Claude לבצע מעבר כזה, ומתי זה שווה.


רקע: איך נראתה גרסת היד

x402 הוא פרוטוקול HTTP 402 Payment Required. הלקוח חותם על הרשאת EIP-3009, והשרת מאמת ומבצע סליקה של עסקה on-chain דרך facilitator.

ה-PaymentHandler הידני נראה בערך כך:

handler = X402::PaymentHandler.new
payment_payload = handler.decode_payment_signature(params[:payment_signature])
requirements = {
  scheme: "exact",
  network: X402::PaymentHandler::NETWORK.call,
  maxAmountRequired: (plan.price_cents * 10_000).to_s,
  payTo: X402::PaymentHandler::WALLET_ADDRESS.call,
  token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  description: "#{plan.key} subscription"
}

verify_result = handler.facilitator.verify(payment_payload, requirements)
unless verify_result["isValid"]
  render json: { error: verify_result["invalidReason"] || "Verification failed" }, status: :unprocessable_entity and return
end

settle_result = handler.facilitator.settle(payment_payload, requirements)
unless settle_result["success"]
  render json: { error: settle_result["errorReason"] || "Settlement failed" }, status: :unprocessable_entity and return
end

כ-30 שורות של אינסטלציה פרוטוקולית בתוך ה-controller: לפענח חתימה, לבנות requirements, verify, settle, לטפל בשגיאות. כתובת חוזה USDC מקובעת בקוד. ה-frontend אותו דבר — window.ethereum.request ידני, מעבר chain ידני, הרכבת header X-PAYMENT ידנית.

הטריגר: הספריות הבשילו

לסרוק שבועית עם Claude את המערכת האקולוגית של הפרוטוקולים שאתה תלוי בהם זו הרגל טוב — במיוחד עבור פרוטוקול טרי כמו x402. Claude יכול לעקוב אחרי התפתחות gem x402-rails (צד Ruby) ו-x402-fetch (צד JS), לראות את הקהילה מתגבשת.

עד שיום אחד:

אתה: "x402-rails ו-x402-fetch בשלים עכשיו? אם כן, העבר."

Claude קורא README ו-changelog ומדווח: פרוטוקול v1 יציב, מצב non-optimistic נותן תוצאת settlement, facilitator ברירת מחדל payai.network. אפשר להעביר.

אחרי המעבר: ה-controller הוא 4 שורות

אותה action subscribe אחרי המעבר:

def subscribe
  plan = Plan.find(params[:plan])

  if Current.user.subscriptions.active.exists?(plan: plan.key)
    render json: { success: true, plan: plan.key, already_active: true }
    return
  end

  x402_paywall(amount: plan.price_dollars)
  return if performed? # ה-gem רינדר 402 או שגיאה, כבר halt

  settlement = request.env["x402.settlement_result"]
  payment    = request.env["x402.payment"]
  return render_failure("settlement failed") unless settlement&.success?

  Subscription.record_x402!(user: Current.user, plan: plan, payment: payment, settlement: settlement)
end

חלק הפרוטוקול כולו בתוך ה-gem. x402_paywall(amount:) פותר בשורה אחת:

  • בקשה ראשונה בלי header X-PAYMENT → ה-gem מרנדר 402 + PaymentRequirements
  • הלקוח x402-fetch חותם הרשאת EIP-3009, מנסה שוב עם X-PAYMENT
  • ה-gem קורא ל-/verify ו-/settle של ה-facilitator (non-optimistic, ממתין ל-settle לפני חזרה)
  • performed? מזהה שה-gem כבר רינדר ואנו עושים return; אחרת request.env["x402.settlement_result"] ו-request.env["x402.payment"] מכילים את התוצאה

אתחול ב-config/initializers/x402.rb (29 שורות):

X402.configure do |config|
  config.wallet_address = Rails.application.credentials.dig(:x402, :wallet_address)
  config.facilitator    = Rails.application.credentials.dig(:x402, :facilitator_url) ||
                          "https://facilitator.payai.network"

  # production → Base mainnet (USDC אמיתי). dev/test → Base Sepolia (USDC testnet חינם)
  config.chain = Rails.env.production? ? "base" : "base-sepolia"

  config.currency   = "USDC"
  config.version    = 1
  config.optimistic = false # המתן ל-settle של facilitator לפני חזרה, כדי לתעד tx_hash סינכרונית
end

זה ליבת התנועה "ידני → ספרייה": 139 שורות services + 220 שורות בדיקות services, ידניות, מוחלפות ב-initializer של 29 שורות + קריאת controller של 4 שורות.

Frontend: viem + x402-fetch, אבל בלי vendor

בצד JS, גרסת היד הרכיבה חתימה לבד וקראה ל-window.ethereum.request ישירות. אחרי המעבר: viem ו-x402-fetch.

אבל שתי החבילות האלה bundled הן מאות KB. vendor (להעתיק dist/ של npm ל-vendor/javascript/) מפוצץ את גודל ה-repo. פתרון: importmap + CDN של jsdelivr + טעינה עצלה:

# config/importmap.rb
pin "viem",        to: "https://cdn.jsdelivr.net/npm/viem/+esm",        preload: false
pin "viem/chains", to: "https://cdn.jsdelivr.net/npm/viem/chains/+esm", preload: false
pin "x402-fetch",  to: "https://cdn.jsdelivr.net/npm/x402-fetch/+esm",  preload: false

preload: false הוא המפתח: הם לא נכנסים ל-<link rel="modulepreload"> של ה-first paint, כך שרוב העמודים כלל לא מורידים אותם.

ב-controller של Stimulus, טען בלחיצה הראשונה על pay:

async loadDeps() {
  if (this._deps) return this._deps
  const [{ wrapFetchWithPayment }, { createWalletClient, custom }, { base, baseSepolia }] =
    await Promise.all([
      import("x402-fetch"),
      import("viem"),
      import("viem/chains")
    ])
  this._deps = { wrapFetchWithPayment, createWalletClient, custom, base, baseSepolia }
  return this._deps
}

משתמשים בלי ארנק לעולם לא מורידים את ה-300+ KB האלה. משתמשים עם MetaMask שלוחצים "שלם" ממתינים פעם אחת ל-jsdelivr (עם CDN cache), לחיצות הבאות מיידיות.

תוך כדי, תוקנו 3 בעיות במימוש הישן

גרסת היד הועתקה ממימוש ייחוס בפרויקט אחר. תוך כדי המעבר, ביקשתי מ-Claude לסרוק ריח שהצטבר. יצאו 3:

1. תפסיק להשתמש ב-selectedAddress

קוד ישן:
js
const address = window.ethereum.selectedAddress

selectedAddress deprecated ב-MetaMask עדכני. הדרך הנכונה:

const accounts = await window.ethereum.request({ method: "eth_requestAccounts" })
const address = accounts[0]

eth_requestAccounts גם מפעיל את דיאלוג החיבור — אם המשתמש לא חיבר ארנק לאתר בעבר, זו דלת ההרשאה.

2. אל תתאים שגיאות לפי מחרוזות

ישן:
js
if (error.message.includes("User rejected")) { ... }
if (error.message.includes("chain")) { ... }

התאמה לפי מחרוזות תמיד נשברת בשינוי הטקסט הבא של הארנק. עבור לקודים ממוינים:

// תקן EIP-1193: 4001 = user rejected
if (error.code === 4001) { this.#showError(this.errorRejectedValue); return }
// קודים מותאמים שחוצים את ה-flow
if (error.code === "CHAIN_SWITCH") { ... }
if (error.code === "PAYMENT_REQUIRED") { ... }

כשאתה זורק שגיאות משלך, הצמד גם code:

throw Object.assign(new Error("no_account"), { code: "NO_ACCOUNT" })

3. מחרוזות UI דרך i18n, בלי אנגלית hardcoded

הקוד הישן החזיק "Connecting wallet..." ואת כל שאר המחרוזות תקועות ב-JS. הועברו לתכונות data-value מוזרקות מ-ERB:

<button data-controller="x402-payment"
        data-x402-payment-label-connecting-value="<%= t('paywall.x402.connecting') %>"
        data-x402-payment-label-signing-value="<%= t('paywall.x402.signing') %>"
        data-x402-payment-error-rejected-value="<%= t('paywall.x402.error.rejected') %>"
        ...>
  <%= t('paywall.x402.pay_button') %>
</button>

JS קורא this.labelConnectingValue. 19 שפות מתרגמות עצמאית. אפס שינויים ב-JS.

שתי מלכודות אמיתיות

המעבר נתקל בשתי מלכודות שאינן קשורות לפרוטוקול x402 עצמו ואינן מתועדות ב-README של ה-gem.

מלכודת 1: importmap מוריד pin בשקט כשאין קובץ vendor

gem x402-rails מגיע עם כמה Stimulus controllers משלו. אחרי התקנה, לחיצה על כפתור תשלום ירקה:

Uncaught Error: no Stimulus controller registered for "x402-pay"

חפרתי. importmap.rb הכיל בבירור:

pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2

אבל vendor/javascript/@hotwired--stimulus.js לא היה קיים. importmap לא זורק שגיאה במצב הזה — פשוט מוריד את ה-pin בשקט. כתוצאה מזה ה-controller של ה-gem לא מוצא את Stimulus, נכשל ברישום, וכל controller שאחריו מת.

תיקון: ספק את קובץ ה-vendor:

./bin/importmap pin @hotwired/stimulus

זה מוריד את חבילת npm ל-vendor/javascript/. סוג כזה של כשל שקט אופייני למה ש-Claude מחמיץ — רואה pin ב-importmap.rb ומניח שהכל בסדר, מבלי לבדוק מיוזמתו אם הקובץ המתאים ב-vendor/javascript/ באמת קיים. בפעם הבאה שתערוך אבחון כזה, בקש מ-Claude לבדוק את שני הקצוות.

מלכודת 2: credentials.yml מפרשת 0x... כמספר שלם

credentials של production, נאיבית:

x402:
  wallet_address: 0xAbCd...

אחרי deploy, כל לחיצה על x402 החזירה 422 עם שגיאה ש-wallet_address לא תואם ל-regex של כתובת EVM.

YAML פירש את 0xAbCd... כמספר שלם הקסדצימלי. בצד Ruby, Rails.application.credentials.dig(:x402, :wallet_address) החזיר Integer ולא String. .to_s שלאחר מכן לפני הכניסה ל-PaymentRequirements המיר אותו למחרוזת עשרונית — כבר לא כתובת חוקית.

התיקון הוא תו אחד — הוסף מרכאות:

x402:
  wallet_address: "0xAbCd..."

מלכודת כזאת Claude לא תופס בהתחלה; צריך לחזור אחורה מהודעת השגיאה עד לשכבת parsing YAML. אחרי שלמדת פעם, הפעם הבאה תרפלקסיבית תוסיף מרכאות סביב כל ערך שמתחיל ב-0x ב-YAML.

צורת הבדיקות משתנה (זה האות החשוב ביותר)

אחרי המעבר, מספר קבצי הבדיקה לא יורד, אבל המיקום זז:

נמחקו:
- test/services/x402/facilitator_client_test.rb (112 שורות)
- test/services/x402/payment_handler_test.rb (108 שורות)

נוספו:
- test/models/purchase_test.rb צבר 40 שורות בדיקות record_x402!
- test/models/subscription_test.rb צבר 69 שורות בדיקות record_x402!

בדיקות שכבת השירות (איך הפרוטוקול עובד) — כולן נעלמו. הוחלפו בבדיקות שכבת ה-model (איך המידע נרשם אחרי תשלום מוצלח).

הגיוני — התנהגות הפרוטוקול שייכת ל-gem, שמבצעת בדיקות לעצמה. אתה צריך לבדוק רק את החלק שכתבת: איך שורת Purchase / Subscription נכנסת אחרי הגעת תוצאת settlement, ואיך tx_hash נשמר.

זה גם האות הקשה של "האם להעביר?": אם בבדיקות שלך יש בלוקים גדולים הטוענים ש"ה-payload שאני שולח בצורה נכונה" או "כשה-facilitator מחזיר isValid=false, אני מטפל כך" — זו התנהגות פרוטוקול, שייכת לספרייה. אם קובץ בדיקה מתחת ל-test/services/ עובר את 100 השורות, רוב הסיכויים שאותו service בודק פרוטוקול / ממשק חיצוני שאמור להיות ספרייה.

מתי לתת ל-Claude לבצע מעבר כזה

לא כל "הקהילה הוציאה gem" שווה מעבר. בקש מ-Claude לשאול תחילה:

  1. מספר גרסת הספרייה. ספריית 0.x עדיין זזה ב-API; 1.x זו נקודת הנעילה.
  2. דלתא של קוד ≥ 200 שורות. שלי הפעם נטו -305 שורות. מתחת ל-100 שורות נטו, עלות ההחלפה לא משתלמת.
  3. איחוד בדיקות אמיתי. אם אחרי המעבר הבדיקות שלך עדיין טוענות 90% מאותם דברים עם סט stubs חדש — ההתנהגות לא עברה לספרייה, רק השם של ה-API השתנה. אל תעביר.
  4. config מתאחד. בגרסה הידנית כתובת חוזה USDC, שם רשת, URL של facilitator פזורים ב-3 מקומות. אחרי: הכל ב-initializer של 29 שורות. זה ערך.
  5. נתיב שדרוג ברור. איך הספרייה תעלה בעתיד? יש מוסכמה של changelog ל-breaking changes? אם לא, עטוף באדפטר משלך שלא ידלוף ה-gem ל-50 אתרי קריאה.

אחרי שה-5 האלה עוברים, prompt המעבר נכנס במשפט אחד:

"gem x402-rails v1 יציב. החלף את PaymentHandler + FacilitatorClient הנוכחיים. השאר את אותם endpoints וצורות תגובה — אני רוצה רק שעבודת הפרוטוקול תיכנס ל-gem. העבר את הבדיקות לשכבת ה-model בהתאם."

Claude יעשה: קרא את תיעוד ה-gem → כתוב initializer → שכתב controller → מחק service ישן → בנה מחדש בדיקות. בדרך יבקש אישור פעמיים-שלוש (למשל "לשמור את ההתנהגות הזאת?"). כשמסיים, הרץ bin/rails test, הכל ירוק, commit.

הלקח

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

נקודת ההחלטה האמיתית:

אותו קובץ בתיקיית services/ שלך — זה שצריך לשנות בכל עדכון של הפרוטוקול — האם יש עכשיו gem שמתחזק את אותו דבר באופן ספציפי?

אם כן, זו לא הלוגיקה העסקית שלך. זה חתול רחוב "מבוית פרוטוקולית" שאימצת לפרויקט. שבועיים להאכיל, רץ טוב — אבל לא שלך. תן ל-Claude להחזיר אותו לקהילה. מה שאתה שומר זה לכתוב את תוצאת הפרוטוקול ל-model שלך — החלק הזה ייחודי לפרויקט שלך.

אחרי המעבר, תיקיית x402 שלי מכילה רק: initializer של 29 שורות + קריאת controller של 4 שורות + שתי מתודות record_x402!. 139 שורות של שכבת שירות ידנית, ו-220 שורות של בדיקות שכבת שירות שבאו איתן — כולן נעלמו. פחות קוד. אותה התנהגות. בדיקות הדוקות יותר. זה מעבר מוצלח.