Skip to main content
LearnGuidesMigrate from Stripe
GUIDE

Migrate from Stripe to Blockchain0x for agent-driven API access.

20 minutes
SHORT ANSWER

Do not replace Stripe; run it alongside. A single decorator gates each protected endpoint behind two auth methods: an active Stripe subscription (for humans) or a validated x402 receipt (for agents). The endpoint returns 402 when neither is present, with both a Stripe Checkout link and an x402 hosted URL. The handler logic does not change.

PREREQUISITES

Before you start.

  • A working Stripe integration with at least one active product/price (subscription or one-off).
  • A Blockchain0x agent profile and API key (see add-payments-to-agent).
  • An auth/middleware layer in your web framework where you currently call Stripe to gate access.
  • A feature-flag mechanism (env var, LaunchDarkly, simple boolean - anything that lets you toggle behavior without redeploy).
  • Understanding of the x402 pattern - the decorator below implements it.
STEP 1 OF 3

Write the dual-auth decorator.

The decorator is the single piece of glue. It checks two things in order: an active Stripe subscription (typical human path) and a validated x402 receipt (typical agent path). If neither matches, it returns 402 with both payment options - humans see the Stripe Checkout link; agents see the x402 hosted URL.

TypeScript (Express)
import express from "express";
import Stripe from "stripe";
import { Blockchain0x } from "@blockchain0x/sdk";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const b0x = new Blockchain0x({ apiKey: process.env.BLOCKCHAIN0X_API_KEY! });

// Auth gate that accepts either Stripe subscription OR a paid 402 settlement.
function requirePayment(priceUsdc: string, reason: string) {
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
    // 1. Human path: Stripe customer with an active subscription.
    const cookieCustomer = req.cookies?.stripe_customer_id;
    if (cookieCustomer) {
      const subs = await stripe.subscriptions.list({ customer: cookieCustomer, status: "active", limit: 1 });
      if (subs.data.length > 0) return next();
    }

    // 2. Agent path: x402 challenge / response.
    const receipt = req.header("X-Blockchain0x-Receipt");
    if (receipt && (await b0x.receipts.validate(receipt))) return next();

    // 3. No valid auth - issue a 402 with both payment options.
    const stripeCheckout = await stripe.checkout.sessions.create({
      mode: "subscription",
      line_items: [{ price: process.env.STRIPE_PRICE_ID!, quantity: 1 }],
      success_url: process.env.APP_URL + "/api/return?session_id={CHECKOUT_SESSION_ID}",
    });
    const agentPayment = await b0x.paymentRequests.create({
      agent_id: process.env.BLOCKCHAIN0X_AGENT_ID!,
      amount_usdc: priceUsdc,
      reason,
      callback_url: process.env.APP_URL + "/webhooks/payment",
    });

    res.status(402).json({
      stripe_checkout_url: stripeCheckout.url,
      x402: {
        hosted_url: agentPayment.hosted_url,
        amount_usdc: priceUsdc,
        expires_at: agentPayment.expires_at,
      },
    });
  };
}
Python (Flask)
from functools import wraps
from flask import request, jsonify
import stripe, os
from blockchain0x import Client

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
b0x = Client(api_key=os.environ["BLOCKCHAIN0X_API_KEY"])

def require_payment(price_usdc: str, reason: str):
    def decorator(fn):
        @wraps(fn)
        async def wrapper(*args, **kwargs):
            # 1. Human path: Stripe subscription cookie.
            customer_id = request.cookies.get("stripe_customer_id")
            if customer_id:
                subs = stripe.Subscription.list(customer=customer_id, status="active", limit=1)
                if subs["data"]:
                    return await fn(*args, **kwargs)

            # 2. Agent path: x402 receipt header.
            receipt = request.headers.get("X-Blockchain0x-Receipt")
            if receipt and b0x.receipts.validate(receipt):
                return await fn(*args, **kwargs)

            # 3. No valid auth - 402 with both options.
            stripe_checkout = stripe.checkout.Session.create(
                mode="subscription",
                line_items=[{"price": os.environ["STRIPE_PRICE_ID"], "quantity": 1}],
                success_url=os.environ["APP_URL"] + "/api/return?session_id={CHECKOUT_SESSION_ID}",
            )
            agent_payment = b0x.payment_requests.create(
                agent_id=os.environ["BLOCKCHAIN0X_AGENT_ID"],
                amount_usdc=price_usdc,
                reason=reason,
                callback_url=os.environ["APP_URL"] + "/webhooks/payment",
            )
            return jsonify({
                "stripe_checkout_url": stripe_checkout.url,
                "x402": {
                    "hosted_url": agent_payment.hosted_url,
                    "amount_usdc": price_usdc,
                    "expires_at": agent_payment.expires_at,
                },
            }), 402
        return wrapper
    return decorator
STEP 2 OF 3

Apply it to the endpoint.

The handler itself does not change - the decorator handles the auth/payment logic, then forwards to the existing implementation only if payment is settled. This is what makes the migration low-risk: human flows are untouched, you have just added an alternate path.

TypeScript
// Apply the decorator to the endpoint.
app.get("/api/premium-feature",
  requirePayment("0.05", "Premium feature call"),
  async (req, res) => {
    const result = await runPremiumFeature();
    res.json(result);
  },
);
Python
@app.get("/api/premium-feature")
@require_payment("0.05", "Premium feature call")
async def premium_feature():
    return await run_premium_feature()
STEP 3 OF 3

Roll out in shadow, then enabled mode.

Do not flip both paths on at once for everyone. The safe rollout pattern is four phases - shadow, silent agent enablement, public agent enablement, observe. The Stripe flow stays unchanged throughout; agent traffic ramps gradually.

# Rollout plan: keep Stripe-only working while you add the agent path.

# Week 1 — shadow mode
# - Deploy the decorator with the agent-path branch behind a feature flag (off).
# - Human Stripe flow continues unchanged.
# - Sandbox-test the agent path against Base Sepolia.

# Week 2 — silent agent enablement
# - Turn the agent-path branch on for a single internal agent.
# - Verify the 402 issues correctly and settlement works end-to-end.
# - Wire alerting on the 402-issued / 402-settled rate.

# Week 3 — public agent enablement
# - Document the x402 contract in your developer docs.
# - Announce to existing customers building agents.
# - Continue measuring: human Stripe flow should be unchanged.

# Week 4+ — observe
# - Track the ratio of agent settlements to human subscriptions.
# - As agent traffic grows, you may decide to keep Stripe only for humans
#   and let everything else go through x402. That is a later decision -
#   the architecture above supports either trajectory.
COMMON PITFALLS

Four mistakes that turn dual-rail painful.

Trying to replace Stripe rather than augment it

Stripe is correct for one-time human checkouts and human subscriptions. Trying to force agent traffic through Stripe (per-call invoices, dynamic pricing) breaks against card-network minimums and fee structures. The successful pattern is augmentation: keep Stripe doing what it is good at (humans paying with cards) and add x402 / agent payments for the traffic Stripe was never designed for. Do not pick one or the other.

Returning 401 to agents instead of 402

Most existing endpoints return 401 Unauthorized when there is no Stripe session. Agents do not know what to do with 401 - they only understand 402 Payment Required as the 'pay to proceed' signal. The decorator must distinguish: 'this caller is unauthorized and cannot pay' (true 401, return 401) versus 'this caller is unauthenticated but can pay' (return 402 with a hosted URL).

Letting agents bypass Stripe pricing

If your Stripe subscription is $20/month for unlimited calls and your x402 quote is $0.01/call, an agent can technically pay $0.01 once and get one call where a human pays $20 for many. That is fine for occasional-use agent traffic and broken for high-volume agent traffic. Set the per-call price so that a heavy agent would naturally hit the subscription threshold - then offer them the option to switch.

Not logging which path was taken

A debugging request comes in: 'this customer says they paid but we did not deliver'. If you have not logged which auth path approved the call, you do not know whether to look at Stripe's records or Blockchain0x's. Always log the decision: which branch matched, what the customer_id or receipt was, and a correlation ID. Without it, every dual-rail incident takes twice as long to triage.

NEXT STEPS

Once the dual-rail is live.

With the architecture in place, the rest is operational. Webhook robustness handles both Stripe and Blockchain0x event streams. Spend controls protect any agents you operate. The pre-launch security review applies just like a single-rail integration.

Full reference at docs.blockchain0x.com. Related product surface: Payment API. Comparison framing: Comparisons.

Last reviewed: 2026-05-15. Published under CC BY 4.0.

Keep Stripe. Add agents.

One decorator. Two payment rails. Zero churn for existing customers. Free to start.