HTTP 402 payments
for Symfony routes

Protect any API endpoint with internet-native payments. One attribute, and unpaid requests get a 402 Payment Required.

Overview

This bundle acts as a Resource Server for the x402 protocol. It does not implement wallets, facilitators, or blockchain logic, payment verification and settlement are delegated to an external x402 facilitator.

Add one attribute to any controller action. If a request arrives without payment, the server responds with HTTP 402, prompting the client to pay and retry.

Installation

composer require berenger/x402-bundle

Register the bundle in config/bundles.php:

return [
    // ...
    X402\Bundle\X402Bundle::class => ['all' => true],
];

Configuration

Create config/packages/x402.yaml:

x402:
    # facilitator_url is optional with a custom FacilitatorClientInterface
    pay_to: '0xYourRecipientAddress'
    network: 'eip155:84532'       # CAIP-2 (Base Sepolia)
    asset: '0xUSDC'               # required for protected routes
    default_amount: '1000'        # required for protected routes
    settle: after                 # after (recommended) | before | manual
    timeout: 10.0                 # seconds
    replay_cache_ttl: 300         # seconds
    max_timeout_seconds: 60       # payment validity window
    asset_name: 'USDC'            # EIP-712 token name
    asset_version: '2'            # EIP-712 token version
    replay:
        cache_pool: cache.app
        # lock_store: app.redis_lock_store  # optional, multi-node

Usage

Protect a controller action with the #[RequiresPayment] attribute:

use X402\Bundle\Attribute\CurrentPayment;
use X402\Bundle\Attribute\RequiresPayment;
use X402\Bundle\Payment\PaymentVerificationResult;

#[RequiresPayment(amount: '1000', description: 'Premium API access')]
public function premium(#[CurrentPayment] PaymentVerificationResult $payment): JsonResponse
{
    return $this->json([
        'data' => 'premium content',
        'payer' => $payment->payer,
    ]);
}

Attribute fields are nullable and fall back to bundle configuration when omitted. Method-level attributes override class-level attributes field by field.

Repeat #[RequiresPayment] on the same action to expose multiple accepts options (networks, amounts).

The bundle dispatches PaymentVerifiedEvent, PaymentSettledEvent, and PaymentFailedEvent for analytics and custom workflows. In manual mode, use PaymentContext from _x402_context and call PaymentSettlementService::settle(); call release() when abandoning a payment.

$context = $request->attributes->get('_x402_context');
if (!$context instanceof PaymentContext) {
    throw new \LogicException('No verified payment context.');
}
$settlement = $paymentSettlementService->settle($context);

Payment Flow

  1. Client requests a protected route No PAYMENT-SIGNATURE header → server responds with HTTP 402 and PAYMENT-REQUIRED header. If a signed payload includes resource.url, it must match the requested URL exactly.
  2. Client retries with payment Sends PAYMENT-SIGNATURE header containing a base64-encoded PaymentPayload.
  3. Bundle verifies payment Calls the facilitator verify endpoint.
  4. Bundle settles Depending on settle mode: after a 2xx response (recommended), before the controller, or manual.
  5. Controller receives payment info Inject #[CurrentPayment] PaymentVerificationResult or read _x402_payment from the request.
  6. Response includes settlement If settlement occurred, PAYMENT-RESPONSE header is added to the response.

Testing on This Project

This demo app links the bundle via a Composer path repository (../x402bundle) and uses a FakeFacilitatorClient, no blockchain or network required.

Available endpoints

GET /api/free                      , public, no payment
GET /api/premium                   , protected, settle: after (default)
GET /api/premium-before            , protected, settle: before
GET /api/premium-manual            , protected, settle: manual
GET /api/premium-broken            , protected, controller returns 500 (after → no charge)
GET /api/premium-multi             , protected, two accepted payment options
GET /api/sample-payment            , returns a valid PAYMENT-SIGNATURE header
GET /api/sample-payment-multi      , returns a header for the Ethereum option
GET /api/sample-payment-invalid    , returns a header that fails verification
GET /api/sample-payment-settle-fail, returns a header whose settlement fails

Live Demo

Click a scenario to call the real API endpoints and see the response.

Run a scenario to see the response