Accept USDC payments in Laravel without requiring buyers to hold ETH
Accepting USDC sounds simple until the customer has enough USDC to pay you, but no ETH in the wallet for the network fee.
Then a payment flow starts looking like this:
Customer has USDC
→ customer clicks Pay
→ wallet requires ETH for gas
→ customer first has to acquire ETH
→ then come back and try again
For someone who just wants to make a payment, that is a lot of blockchain knowledge.
In this tutorial, we'll use P2Flux for Laravel to create a USDC payment on Base where the buyer can also pay the network cost in USDC.
The Laravel application will still verify the transaction server-side before marking an order as paid.
P2Flux is non-custodial: the merchant payment settles directly to the merchant wallet rather than sitting in a P2Flux balance waiting to be withdrawn.
Install the Laravel package
P2Flux has an official integration for Laravel 12 and 13.
composer require p2flux/laravel
Laravel discovers the package automatically, so there is no service provider to register manually.
You also don't need a P2Flux API key.
If you want to publish the configuration file:
php artisan vendor:publish --tag=p2flux-config
The default configuration points to the production P2Flux API:
return [
'api_url' => env('P2FLUX_API_URL', 'https://api.p2flux.com'),
'timeout' => (int) env('P2FLUX_TIMEOUT', 60),
];
The package intentionally stays small.
Installing it does not add:
- payment tables
- migrations
- routes
- controllers
- scheduled jobs
- queue workers
Your Laravel application remains responsible for its own orders, subscriptions and business logic.
Inject P2Flux into your controller
The recommended approach is normal Laravel dependency injection.
<?php
namespace App\Http\Controllers;
use P2Flux\P2FluxClient;
final class PaymentController
{
public function __construct(
private readonly P2FluxClient $p2flux,
) {
}
}
The Laravel package registers P2FluxClient as a singleton.
There is also an optional facade:
use P2Flux\Laravel\Facades\P2Flux;
$capabilities = P2Flux::capabilities();
I prefer constructor injection because it makes the dependency explicit and is easier to replace in tests.
Check whether sponsored payments are supported
Before relying on payment-token sponsorship, check the capabilities exposed by P2Flux.
$capabilities = $this->p2flux->capabilities();
On Base, P2Flux can support a payment mode where the network cost is paid using USDC instead of requiring the buyer to separately hold ETH.
Conceptually:
Normal USDC payment
buyer pays USDC
+
buyer needs ETH for network gas
With payment-token sponsorship:
buyer pays USDC
+
network cost is paid in USDC
+
P2Flux relayer supplies the required Base ETH
This is not a gas-free transaction.
The blockchain still has a network cost.
The difference is that the buyer does not need to acquire and hold another asset just to pay it.
Create the payment from your Laravel backend
Payment creation should happen on your backend, next to the order the payment belongs to.
A typical application flow is:
POST /checkout
→ create merchant order
→ create P2Flux payment
→ store the P2Flux reference against the order
→ return/open hosted checkout
For the sponsored flow, the payment uses:
gas_payment_mode = payment_token
The official Laravel package contains a complete controller for this:
examples/Http/Controllers/CreateSponsoredPaymentController.php
The important Laravel pattern is:
<?php
namespace App\Http\Controllers;
use P2Flux\P2FluxClient;
final class CreateSponsoredPaymentController
{
public function __construct(
private readonly P2FluxClient $p2flux,
) {
}
public function __invoke()
{
$capabilities = $this->p2flux->capabilities();
// 1. Check that payment-token sponsorship is available
// for the network/token you intend to use.
// 2. Create the P2Flux payment for your order
// using payment_token sponsorship.
// 3. Store the P2Flux intent/reference
// against your own Order model.
// 4. Return the hosted checkout URL to the browser.
}
}
I recommend copying the current working controller from the official repository rather than copying payment-field definitions from an article.
The repository examples are tested together with the package and stay aligned with the PHP SDK.
What happens in the browser?
The buyer opens the P2Flux hosted checkout and completes the wallet interaction there.
When checkout appears to finish, the browser receives a completion message.
The flow looks roughly like this:
Laravel backend
→ P2Flux hosted checkout
→ buyer signs/pays
→ checkout reports completion
→ browser sends that claim back to Laravel
But there is an important rule:
Do not mark an order as paid because the browser said the checkout completed.
The browser message is only a claim.
Do not do this:
if (event.data.type === 'p2flux.payment.completed') {
markOrderAsPaid();
}
Instead:
browser reports completion
→ Laravel receives transaction/payment information
→ Laravel verifies it with P2Flux
→ Laravel marks the order paid only after verification
Verify the payment on the server
Your Laravel backend should be the authority that changes the order state.
A simplified controller looks like this:
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use P2Flux\P2FluxClient;
final class VerifyPaymentController
{
public function __construct(
private readonly P2FluxClient $p2flux,
) {
}
public function __invoke(Request $request, Order $order)
{
return DB::transaction(function () use ($request, $order) {
$order = Order::query()
->whereKey($order->getKey())
->lockForUpdate()
->firstOrFail();
if ($order->paid_at !== null) {
return response()->json([
'paid' => true,
]);
}
/*
* Call P2Flux verification here using the
* payment data/reference stored for this order.
*
* Only after P2Flux returns an authoritative
* valid result should the order become paid.
*/
return response()->json([
'paid' => $order->paid_at !== null,
]);
});
}
}
The official package includes the full implementation:
examples/Http/Controllers/VerifyPaymentController.php
That example also covers recovery and repeat-safe order updates.
The important principle is:
Browser result = claim. Server verification = truth.
Make payment fulfilment idempotent
Verification may happen more than once.
For example:
buyer completes checkout
→ browser sends result
→ browser retries
→ customer refreshes
→ background recovery sees the same payment later
Your application should safely reach the same final state every time.
That is why the example uses a database transaction and row lock.
Conceptually:
if ($order->paid_at !== null) {
return;
}
// Verify through P2Flux.
$order->paid_at = now();
$order->save();
For a real application, you will probably also persist things such as:
your order reference
P2Flux payment reference
transaction hash
settlement information
payment timestamp
according to your own accounting requirements.
P2Flux does not try to replace your application's order database.
What if the request times out?
Payments have another interesting backend problem.
Imagine:
Laravel sends an operation
→ blockchain transaction is submitted
→ HTTP connection disappears
→ Laravel never receives the response
The dangerous response would be:
"Didn't get a response. Just send it again."
For a money-moving operation, that can create duplicate behavior.
P2Flux therefore has recovery operations.
The Laravel examples include patterns around:
recoverPayment()
and:
recoverCharge()
The safer pattern is:
request result is ambiguous
→ recover the existing operation
→ determine what already happened
→ only then decide whether another action is required
This becomes especially important for recurring payments.
What does the buyer need?
For a sponsored USDC payment on Base:
Buyer needs:
USDC
They do not need to separately hold:
ETH just for the network fee
And they do not need:
a P2Flux account
P2Flux's relayer supplies the ETH required for the sponsored transaction.
The corresponding network cost is paid in USDC.
The merchant payment itself remains direct wallet-to-wallet settlement.
Recurring USDC payments
P2Flux also supports recurring payments.
The Laravel package deliberately does not install its own scheduler.
Your Laravel application decides when a subscription should be charged.
For example:
Laravel scheduler
→ find subscriptions due for collection
→ call P2Flux charge()
→ record the result
→ recover ambiguous results when necessary
The package includes an application-level Artisan command showing this pattern:
examples/Console/ChargeDueSubscriptions.php
There is also a recovery command for pending payments.
This keeps responsibilities clear:
Laravel application:
billing schedule
customer/order state
business rules
P2Flux:
payment execution
verification
recovery
Cancellation and allowance restoration
The Laravel repository also contains examples for subscription lifecycle operations.
These include:
CancelSubscriptionController
RestoreAllowanceController
Again, these are application examples.
Installing p2flux/laravel does not automatically register controllers or routes in your project.
You copy/adapt only the pieces your application needs.
Refunds
Refund handling is another place where repeat safety matters.
The package contains a RefundController example that demonstrates the application-side pattern while protecting against duplicate or stranded refund operations.
The Laravel integration itself does not create a refund database or change your application models.
Your application remains responsible for recording the relationship between:
order
original payment
refund state
refund transaction
Test your integration without spending crypto
You should not need a real wallet or Mainnet transaction to test application logic.
The underlying P2Flux PHP SDK supports an injectable transport.
That means your Laravel tests can replace the normal client with a client using a fake transport and simulate responses such as:
payment valid
payment confirming
payment rejected
network failure
rate limit
recovery success
recovery unavailable
recurring collection
already charged
The general Laravel pattern is:
$this->app->instance(
P2FluxClient::class,
$fakeClient
);
Then test what your application does with each payment result.
No USDC or ETH needs to move.
The official Laravel package itself uses this approach extensively in its automated test suite.
Laravel production configuration
The package supports Laravel's normal production optimization flow.
For example:
php artisan config:cache
php artisan optimize
P2Flux configuration continues to resolve correctly after config caching.
You can also see the active package configuration using:
php artisan about
The P2Flux section reports non-secret operational information such as the API URL, timeout and installed PHP SDK version.
Install it
To add P2Flux to a Laravel 12 or Laravel 13 application:
composer require p2flux/laravel
The official PHP SDK is installed automatically as a dependency.
P2Flux is live on Base Mainnet and currently supports workflows including:
one-time USDC payments
recurring payments
hosted checkout
server-side verification
payment recovery
refunds
subscription cancellation
allowance restoration
network fees paid in USDC
The Laravel package is intentionally only the Laravel integration layer.
The underlying payment behavior stays in the official P2Flux PHP SDK.
Links
Laravel package on Packagist
https://packagist.org/packages/p2flux/laravel
Laravel package source, documentation and examples
https://github.com/P2Flux/laravel
Official PHP SDK
https://github.com/P2Flux/sdk-php
P2Flux documentation
P2Flux
The Laravel integration is new, so feedback from developers working on real Laravel payment flows is very welcome.