بروتوكولان مختلفان تماماً في تطبيق واحد — Checkout مستضاف من Stripe مع webhook، وHTTP 402 مع محفظة المتصفح من x402. ثلاثة فخاخ صامتة، بنية واحدة تشغل المسارين معاً.
ربطت مؤخراً كلاً من Stripe (بطاقات/عملات قانونية) وx402 (USDC على EVM on-chain) في خطة Pro لـ how2claude. ترك Claude يكتب تكاملات لبروتوكولين مختلفين تماماً — أحدهما Checkout مستضاف + webhook، والآخر HTTP 402 + محفظة المتصفح — استغرق جلسة مسائية كاملة. تعثرت في ثلاثة إخفاقات صامتة، وانتهيت ببنية تشغّل المسارين معاً.
هذا ليس درس "كيف تدمج Stripe" — تلك متوفرة في كل مكان. الجزء الممتع: كيف يجلس البروتوكولان جنباً إلى جنب، أين يصعب على Claude أن لا يخطئ، وفي أي لحظات يجب أن تجلس أنت بنفسك وتراقب.
| البُعد | Stripe | x402 |
|---|---|---|
| المُشغِّل | button_to → إعادة توجيه إلى checkout.stripe.com | POST /x402/subscribe → يُرجع HTTP 402 |
| إجراء المستخدم | إدخال البطاقة في صفحة Stripe المستضافة | التوقيع في محفظة المتصفح |
| تسليم النتيجة | webhook (checkout.session.completed) | إعادة الطلب مع header X-PAYMENT، الـ gem يسوّي بشكل متزامن |
| البيانات التي أحتاج لحفظها | payment_intent_id + amount_total | tx_hash + payer + amount |
| تعقيد البروتوكول | الـ SDK يفعل كل شيء | يحتاج مصافحة بروتوكول viem + x402-fetch |
مختلفان جوهرياً: Stripe يدفع المستخدم إلى صفحته الخاصة وأنت تتحقق فقط من webhook عند عودته؛ x402 يبقى كلياً على نطاقك من البداية إلى النهاية، يجري مصافحة البروتوكول على طبقة HTTP.
هذا التمييز يقود كل قرار معماري أدناه.
في البداية كانت الـ controllers محشوة بربط الحقول:
# ❌ الإصدار المبكر
def subscribe_via_stripe
session = Stripe::Checkout::Session.retrieve(params[:session_id])
Subscription.create!(
user: current_user,
provider: "stripe",
stripe_subscription_id: session.subscription,
# ... عشرة أسطر أو أكثر من ربط الحقول
)
end
كلا المسارين يحفظان Purchase + Subscription، لكن الحقول مختلفة تماماً. ربط الحقول في الـ controller يعني أن كل مسار ينسخ منطق الربط.
الترحيل (9f3e239) دفعها إلى الـ model:
class Purchase < ApplicationRecord
validates :provider, presence: true, inclusion: { in: %w[stripe x402] }
def self.record_x402!(article:, user:, payment:, settlement:)
create!(
article: article,
user: user,
provider: "x402",
wallet_address: payment[:payer],
amount_cents: article.price_cents,
tx_hash: settlement.transaction,
purchased_at: Time.current
)
end
def self.record_stripe!(session:, user:)
create!(
article_id: session.metadata.article_id,
user: user,
provider: "stripe",
amount_cents: session.amount_total,
stripe_payment_intent_id: session.payment_intent,
purchased_at: Time.current
)
end
end
أربع طرق إجمالاً: Purchase.record_x402! / record_stripe! / Subscription.record_x402! / record_stripe!. الـ controller يصبح سطراً واحداً:
Purchase.record_x402!(article:, user:, payment:, settlement:)
Claude رائع في هذا النوع من العمل: سيربط بإخلاص كل حقل، ويضيف اختبارات، ويضيف validates :provider, inclusion: { in: %w[stripe x402] }. البشر يميلون إلى "اجعله يعمل أولاً"، فيظل ربط الحقول مبعثراً عبر الـ controllers، لا يفلت أبداً.
في b2f0333 جعلت Claude يكتب تكامل x402 الأول يدوياً — ثلاث فئات:
X402::PaymentHandler — بناء 402 requirements، فك تشفير header PAYMENT-SIGNATUREX402::FacilitatorClient — تغليف /verify + /settle لـ x402.org/facilitatorapp/controllers/concerns/content_gate.rb — اكتشاف header 402، إرجاع PAYMENT-REQUIRED449 سطراً، يعمل، اختبارات تجتاز.
بعد ست ساعات (9f3e239) جعلته يستبدل كل شيء بـ gem x402-rails (بروتوكول v1، وضع غير-تفاؤلي). حذفت الفئات الثلاث؛ الـ controllers الآن تستخدم DSL x402_paywall(amount:) وتقرأ من request.env["x402.payment"] وrequest.env["x402.settlement_result"].
الإيقاع مهم: الكتابة اليدوية أولاً تجعلك تفهم البروتوكول، ثم الـ gem يحررك. إن بدأت بالـ gem، Claude يكتب بناءً على وثائق الـ gem ولن تعرف ما الذي يوجد فعلاً في header 402 أو ما يفعله /settle. حين ينكسر شيء (دائماً ينكسر شيء)، ليس لديك أرضية للتنقيح.
هذا النمط يصلح لأي بروتوكول/خدمة جديدة: اجعل Claude يكتب يدوياً مرة، اخضّر الاختبارات، ثم اجعله يستبدل بالـ gem. الـ diff بين الاثنين هو مادتك الدراسية.
x402 initializer (config/initializers/x402.rb) يضع القاعدة hardcoded:
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 (real USDC). Dev/test → Base Sepolia (free testnet USDC).
config.chain = Rails.env.production? ? "base" : "base-sepolia"
config.currency = "USDC"
config.version = 1
config.optimistic = false # انتظر تسوية facilitator قبل المتابعة، لنلتقط tx_hash بشكل متزامن
end
نفس الكود: dev يعمل base-sepolia (رموز اختبار مجانية)، prod يعمل base mainnet. لا شيء يتغير عند النشر. (هذا المبدأ جاء من المقال السابق ترك Claude ينشر إلى الإنتاج — أي شيء يختلف بين dev وprod، قلّبه عبر Rails.env.)
سطر optimistic = false مهم: وضع الـ gem الافتراضي optimistic يمرر الطلب ويوفّق لاحقاً؛ نطفئه لأننا نريد settlement_result.transaction (الـ tx_hash) قبل أن ترجع الـ action، لكتابته متزامناً إلى صف Purchase. صف Purchase بدون tx_hash لا قيمة له للمستخدم — يريد أن ينقر ليرى المعاملة على BaseScan.
"الواجهة الأمامية" لـ Stripe سطر واحد:
<%= button_to stripe_checkouts_subscription_path(plan: plan.key),
class: "...",
form: { class: "w-full", data: { turbo: false } } do %>
<%= t("pricing.subscribe") %>
<% end %>
المستخدم ينقر، المتصفح يقفز إلى checkout.stripe.com. صفر كود واجهة أمامية على جانبك.
جانب x402 (93746d8) احتاج Stimulus controller:
// app/javascript/controllers/x402_payment_controller.js
async pay() {
// تحميل كسول — لا تنفخ vendor bundle
const viem = await import("https://esm.run/viem@2")
const { wrapFetchWithPayment } = await import("https://esm.run/[email protected]")
const [account] = await window.ethereum.request({ method: "eth_requestAccounts" })
const walletClient = viem.createWalletClient({ account, transport: viem.custom(window.ethereum) })
const fetchWithPayment = wrapFetchWithPayment(fetch, walletClient)
const res = await fetchWithPayment(this.endpointValue, {
method: "POST",
headers: { "Accept": "application/json" },
body: new URLSearchParams(this.paramsValue)
})
// ...
}
شيئان جديران بالملاحظة:
eth_requestAccounts، لا selectedAddress. selectedAddress deprecated ومعظم المحافظ تُرجع قيمة قديمة. أول إصدار من Claude استخدم selectedAddress (وفقاً لوثائق MDN)؛ غيّرته.شيء آخر: عدّد أكواد الأخطاء. رفض المحفظة للتوقيع هو 4001، السلسلة الخاطئة تحتاج تبديل هي CHAIN_SWITCH، الدفع المطلوب هو PAYMENT_REQUIRED. لا تطابق سلاسل ضد error.message — المحافظ تكتب بطرق مختلفة ولا يمكنك كتابة اختبارات ضدها.
التزام 527f700 هو الذي قضيت نصف ساعة أحدّق في المتصفح لأجده.
العَرَض: انقر زر Subscribe في /pricing، لا يحدث شيء. لا خطأ في console، لا خطأ شبكة. سجل Rails يُظهر 200 يُعيد 302 → checkout.stripe.com/c/pay/cs_xxx. المتصفح لا يتحرك.
السبب: button_to يُنشئ <form method="post">، وTurbo يعترض إرسال النموذج، يعامل الاستجابة كـ TURBO_STREAM. TURBO_STREAM لا يتبع 302 cross-origin. الاستجابة تُبتلع بصمت من قبل Turbo؛ الصفحة تبقى كما هي.
الإصلاح:
<%= button_to stripe_checkouts_subscription_path(plan: plan.key),
class: "...",
- form: { class: "w-full" } do %>
+ form: { class: "w-full", data: { turbo: false } } do %>
ثلاثة أزرار متأثرة: Subscribe في /pricing، زر Manage في بطاقة "current plan" في /pricing (يقفز إلى billing.stripe.com)، وManage Subscription في /accounts. كل واحد حصل على data-turbo=false واختبار انحدار.
عندما طلبت من Claude تنقيح هذا، استكشف ثلاث اتجاهات خاطئة: تكوين Stripe (لا)، قائمة بيضاء redirect_uri (لا)، CORS (اتجاه خاطئ). تعارض Turbo/Stripe ليس في وثائق Stripe ولا وثائق Turbo — وتقريباً لا شيء عنه في بيانات تدريب Claude. هذا النوع من الفخاخ تصطاده فقط برؤية 302 يعود في علامة تبويب network ثم تسأل نفسك "إذن لماذا لم يتبعه المتصفح؟".
بعد تثبيت gem x402-rails، console المتصفح:
Uncaught TypeError: Failed to resolve module specifier 'x402-fetch'.
لكنني أحمّل صراحةً عبر await import("https://esm.run/[email protected]") — URL كاملة — فلماذا "resolve module specifier"؟
السبب الجذري: gem x402-rails يأتي مع Stimulus controller يعتمد على @hotwired/stimulus. ثبّتت تلك الحزمة في config/importmap.rb، لكن ملف vendor المقابل vendor/javascript/@hotwired--stimulus.js لم يُحمَّل أبداً. importmap يلاحظ أن الملف مفقود ويُسقط الـ pin بصمت من الـ importmap المُولَّد. الذي يفشل ليس x402-fetch الخاص بي؛ هو Stimulus controller الخاص بالـ gem. الخطأ يفقّع إلى أقرب import.
التشخيص: bin/importmap json يُخرج الـ importmap المُولَّد فعلياً. قارن بـ config/importmap.rb — أي pin غائب من json يعني أن ملف vendor الخاص به لم يُحمَّل.
الإصلاح: bin/importmap pin @hotwired/stimulus لجلب الملف فعلياً.
Claude لا يشغّل bin/importmap json انعكاسياً كفحص سلامة بعد تثبيت gem. هذا عليك. إن كنت تستخدم importmap، بعد تثبيت أي gem يأتي مع Stimulus controllers، شغّل bin/importmap json مرة وأكّد أن لا pin أُسقط بصمت.
في credentials:
x402:
wallet_address: 0x1234abcd...
عندما يحمّل Rails هذا، YAML يحلل 0x1234abcd... كـ عدد صحيح (hex literal). بحلول الوقت الذي يصل فيه X402.configure إلى القيمة، النوع مكسور، والـ gem يُنتج paywall requirements غريبة.
إصلاح بحرف واحد: أضف اقتباس.
x402:
wallet_address: "0x1234abcd..."
Claude لم يضع اقتباساً عند كتابة قالب credentials — بيانات تدريبه مليئة بأمثلة YAML بسلاسل عارية. تتفعّل فقط حين يكون البادئة بالصدفة 0x / true / false / أرقاماً. هذا النوع من فخاخ "تحليل YAML الخاص" يتفعّل فقط حين تملأ قيماً حقيقية.
Stripe يغطي 99% من المستخدمين — بطاقة ائتمان / Apple Pay / Google Pay. لتدفق $9.99/شهر، التجربة لا تُهزم.
x402 يغطي الـ 1% المتبقي من الأشخاص المهمين: مستخدمي العملات المشفرة الأصليين، المستخدمين الدوليين الذين يريدون عملات مستقرة، والمطورين الذين يكتبون agents آلية (والتي تحتاج agentاتهم القدرة على الدفع للوصول إلى APIs مدفوعة — لذلك صُمم 402).
قرار منتجي حاسم: الخطة الشهرية لا تحصل على x402. $9.99/شهر مع توقيع محفظة كل شهر هي تجربة مستخدم فظيعة. نفعّل x402 فقط في $99 السنوية، حيث يتوزع الاحتكاك إلى مرة واحدة في السنة.
<% if plan.interval == "year" %>
<%= render "shared/x402_pay_button", ... %>
<% end %>
if واحد في _plan_card.html.erb يحدد أي البطاقات تُظهر زر USDC. بهذه البساطة.
ترك Claude يدمج المدفوعات — قائمة كاملة:
inclusion: { in: %w[stripe x402] } كبوابة نوع.Rails.env.production?.data-turbo=false. وإلا فإن Turbo يبتلع بصمت 302 cross-origin.bin/importmap json. importmap يُسقط بصمت pins ملفات vendor الخاصة بها مفقودة.0x... / true / 07 تخضع لتحليل YAML خاص.الجزء الصعب من ترك Claude يكتب المدفوعات ليس البروتوكولات نفسها — بل حدود التكامل (Turbo ضد Stripe، importmap ضد gem، YAML ضد عنوان المحفظة). هذه هي اللحظات التي يجب أن تجلس فيها بنفسك.