الهجرة من اليدوي إلى gem: -622/+317 سطراً صافياً. الـ controller ينزل من 30 سطر سباكة بروتوكول إلى 4. فخاخ: importmap يُسقط pin بصمت، YAML يقرأ 0x... كعدد صحيح.
diff لأحد commits:
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 مثبّت في الكود. الواجهة الأمامية مثل ذلك — window.ethereum.request مكتوب يدوياً، تبديل chain يدوي، تركيب header X-PAYMENT يدوي.
جعل Claude يمسح أسبوعياً نظام البروتوكولات التي تعتمد عليها عادة جيدة — خصوصاً لبروتوكول مثل x402 ظهر حديثاً. يمكن لـ Claude أن يتابع تطور gem x402-rails (جانب Ruby) وx402-fetch (جانب JS)، وأن يرى المجتمع يتشكّل.
حتى يوم ما:
أنت: "هل
x402-railsوx402-fetchناضجتان الآن؟ إن كانتا كذلك، فرحِّل."
يقرأ Claude README وchangelogs ويعود بالتقرير: بروتوكول 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 أو خطأ، وتوقف بالفعل
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 الشبكة الرئيسية (USDC حقيقي). dev/test → Base Sepolia (USDC مجاني للاختبار)
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.
لكن هاتين الحزمتين مجمّعتين بمئات الكيلوبايتات. عمل vendor (نسخ dist/ من npm إلى vendor/javascript/) يُفجّر حجم المستودع. الحل: 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"> الخاص بالرسم الأول، فمعظم الصفحات لا تنزله أصلاً.
في 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+ كيلوبايت أبداً. المستخدمون الذين لديهم MetaMask ويضغطون "ادفع" ينتظرون مرة من jsdelivr (بذاكرة CDN)، والنقرات التالية فورية.
النسخة اليدوية منسوخة من تنفيذ مرجعي في مشروع آخر. أثناء الترحيل، جعلت 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 خاصة به. بعد تثبيت الـ gem، أدت الضغطة على زر الدفع إلى:
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 الإنتاج، مكتوبة بسذاجة:
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 هو وقت التثبيت.عند استيفاء هذه الخمسة، prompt الترحيل جملة واحدة:
"
x402-railsgem 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 سطراً من اختبارات الخدمة المصاحبة لها — اختفت كلها. كود أقل. نفس السلوك. اختبارات أكثر إحكاماً. هذا ترحيل ناجح.