מיגרציה מכתוב-יד → 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. אפשר להעביר.
אותה 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:) פותר בשורה אחת:
X-PAYMENT → ה-gem מרנדר 402 + PaymentRequirementsx402-fetch חותם הרשאת EIP-3009, מנסה שוב עם X-PAYMENT/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 שורות.
בצד 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), לחיצות הבאות מיידיות.
גרסת היד הועתקה ממימוש ייחוס בפרויקט אחר. תוך כדי המעבר, ביקשתי מ-Claude לסרוק ריח שהצטבר. יצאו 3:
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 גם מפעיל את דיאלוג החיבור — אם המשתמש לא חיבר ארנק לאתר בעבר, זו דלת ההרשאה.
ישן:
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" })
הקוד הישן החזיק "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.
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 לבדוק את שני הקצוות.
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 בודק פרוטוקול / ממשק חיצוני שאמור להיות ספרייה.
לא כל "הקהילה הוציאה gem" שווה מעבר. בקש מ-Claude לשאול תחילה:
0.x עדיין זזה ב-API; 1.x זו נקודת הנעילה.אחרי שה-5 האלה עוברים, prompt המעבר נכנס במשפט אחד:
"gem
x402-railsv1 יציב. החלף את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 שורות של בדיקות שכבת שירות שבאו איתן — כולן נעלמו. פחות קוד. אותה התנהגות. בדיקות הדוקות יותר. זה מעבר מוצלח.