Accept USDC payments in Laravel without requiring buyers to hold ETH

php dev.to

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
];
Enter fullscreen mode Exit fullscreen mode

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,
    ) {
    }
}
Enter fullscreen mode Exit fullscreen mode

The Laravel package registers P2FluxClient as a singleton.

There is also an optional facade:

use P2Flux\Laravel\Facades\P2Flux;

$capabilities = P2Flux::capabilities();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

With payment-token sponsorship:

buyer pays USDC
+
network cost is paid in USDC
+
P2Flux relayer supplies the required Base ETH
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For the sponsored flow, the payment uses:

gas_payment_mode = payment_token
Enter fullscreen mode Exit fullscreen mode

The official Laravel package contains a complete controller for this:

examples/Http/Controllers/CreateSponsoredPaymentController.php
Enter fullscreen mode Exit fullscreen mode

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.
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Instead:

browser reports completion
→ Laravel receives transaction/payment information
→ Laravel verifies it with P2Flux
→ Laravel marks the order paid only after verification
Enter fullscreen mode Exit fullscreen mode

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,
            ]);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The official package includes the full implementation:

examples/Http/Controllers/VerifyPaymentController.php
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

For a real application, you will probably also persist things such as:

your order reference
P2Flux payment reference
transaction hash
settlement information
payment timestamp
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The dangerous response would be:

"Didn't get a response. Just send it again."
Enter fullscreen mode Exit fullscreen mode

For a money-moving operation, that can create duplicate behavior.

P2Flux therefore has recovery operations.

The Laravel examples include patterns around:

recoverPayment()
Enter fullscreen mode Exit fullscreen mode

and:

recoverCharge()
Enter fullscreen mode Exit fullscreen mode

The safer pattern is:

request result is ambiguous
→ recover the existing operation
→ determine what already happened
→ only then decide whether another action is required
Enter fullscreen mode Exit fullscreen mode

This becomes especially important for recurring payments.

What does the buyer need?

For a sponsored USDC payment on Base:

Buyer needs:

USDC
Enter fullscreen mode Exit fullscreen mode

They do not need to separately hold:

ETH just for the network fee
Enter fullscreen mode Exit fullscreen mode

And they do not need:

a P2Flux account
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The package includes an application-level Artisan command showing this pattern:

examples/Console/ChargeDueSubscriptions.php
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Cancellation and allowance restoration

The Laravel repository also contains examples for subscription lifecycle operations.

These include:

CancelSubscriptionController
RestoreAllowanceController
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The general Laravel pattern is:

$this->app->instance(
    P2FluxClient::class,
    $fakeClient
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

P2Flux configuration continues to resolve correctly after config caching.

You can also see the active package configuration using:

php artisan about
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

https://p2flux.com/docs/

P2Flux

https://p2flux.com

The Laravel integration is new, so feedback from developers working on real Laravel payment flows is very welcome.

Source: dev.to

arrow_back Back to Tutorials