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
-
Client requests a protected route No
PAYMENT-SIGNATUREheader → server responds with HTTP 402 andPAYMENT-REQUIREDheader. If a signed payload includesresource.url, it must match the requested URL exactly. -
Client retries with payment Sends
PAYMENT-SIGNATUREheader containing a base64-encodedPaymentPayload. -
Bundle verifies payment Calls the facilitator
verifyendpoint. -
Bundle settles Depending on
settlemode:aftera 2xx response (recommended),beforethe controller, ormanual. -
Controller receives payment info Inject
#[CurrentPayment] PaymentVerificationResultor read_x402_paymentfrom the request. -
Response includes settlement If settlement occurred,
PAYMENT-RESPONSEheader 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.