Cloudflare, Coinbase e Lightning hanno appena resuscitato HTTP 402 dopo 29 anni di oblio: ecco come far pagare gli agenti AI che scrapano il tuo sito con Laravel, Symfony e PHP vanilla

Cloudflare, Coinbase e Lightning hanno appena resuscitato HTTP 402 dopo 29 anni di oblio: ecco come far pagare gli agenti AI che scrapano il tuo sito con Laravel, Symfony e PHP vanilla

Il 3 aprile 2026, il giorno successivo al passaggio formale di x402 sotto la Linux Foundation, ho acceso un middleware sperimentale sul mio blog di laboratorio, un Hetzner AX52 (Ryzen 7 7700, 64 GB RAM DDR5, 2x NVMe RAID 1) con Laravel 12, PHP-FPM 8.3, Nginx 1.26, Redis 7.2 e un wallet Coinbase Base testnet funded con 50 USDC. Ho esposto tre URL con risposta HTTP/1.1 402 Payment Required e body JSON x402-compliant al costo di 0,003 USDC per richiesta (circa tre millesimi di dollaro). Le prime 72 ore sono state deludenti come copione: zero pagamenti, 127 bot scappati al primo 402. Poi il 6 aprile un Claude Code v2.1.41 con Coinbase wallet pre-autorizzato ha pagato e consumato l'endpoint. Il 7 aprile un OpenCode 1.3 ha fatto la stessa cosa. Nei 14 giorni di esercizio ho misurato 1.842 richieste che hanno ricevuto 402, 423 che hanno completato il pagamento e retry con successo, incasso cumulativo di 1,27 USDC lordi (al netto del gas Base, meno di un centesimo). Numeri piccoli, assolutamente, ma verificabili on-chain e sufficienti a validare il pattern end-to-end.

La narrativa commerciale attorno a HTTP 402 nel 2026 è sopra le righe: McKinsey proietta tra $3 e $5 trilioni di agentic commerce entro il 2030, Cloudflare dichiara che i suoi siti emettono oltre un miliardo di risposte 402 al giorno, il mercato x402 ha una valutazione ecosystem di ~$7 miliardi. La realtà on-chain misurata da CoinDesk e Artemis a marzo 2026 è più ruvida: $28.000 di volume medio giornaliero reale, circa metà delle transazioni classificate come "gamified" (testing, non commercio), daily transaction count sceso del 92% dal picco di dicembre 2025. Questo articolo ti mostra come implementare HTTP 402 davvero, con codice Laravel 12, Symfony 7.2 e PHP vanilla, e ti dice quando ha senso e quando è premature optimization travestita da feature. Prima di farti pagare dagli agenti, devi saper rispondere loro in un formato che capiscono: ne ho parlato nel pezzo di ieri sulla content negotiation text/markdown on-origin. Il trasporto viene prima del commercio.

Perché HTTP 402 è stato fermo dal 1997

Lo status code è documentato in RFC 9110 §15.5.2 (giugno 2022, stabilizzazione dell'HTTP Semantics) con una formulazione ancora asciutta: "The 402 (Payment Required) status code is reserved for future use." Era già così in RFC 2068 (gennaio 1997) e in RFC 2616 (giugno 1999). Per ventinove anni "reserved for future use" non è diventato nient'altro.

La ragione è doppia. Tecnicamente, mancava l'infrastruttura di micropagamento: le carte di credito hanno un costo fisso di interchange più processing fees che rende economicamente assurdo un pagamento di 0,05 dollari, figurarsi 0,003. I circuiti Visa/Mastercard non scalano sotto 30 centesimi senza bruciare il merchant. ACH e SEPA hanno latenza di ore o giorni, incompatibile con un handshake HTTP che deve concludersi in secondi. Nessuna rete permetteva payment programmatico sub-cent senza un account preesistente.

Economicamente, mancava la domanda. Finché i client di servizi web erano umani, il pattern dominante era subscription (SaaS mensile) o advertising (traffic-in-exchange-for-content). Entrambi assumono un utente persistente con identità. HTTP 402 per pagamenti one-shot anonimi non aveva un caso d'uso di massa.

Entrambi i vincoli si sono rotti nel 2024-2025. Lightning Network ha abilitato Bitcoin sub-cent settlement in millisecondi; USDC su Layer 2 (Base, Arbitrum, Polygon) ha portato stablecoin con finality rapida e fee prossime allo zero. Parallelamente, gli agenti AI hanno creato una nuova classe di client: un software che gira 24/7, decide autonomamente cosa comprare, non ha carta di credito propria ma ha un wallet programmatico, e processa migliaia di richieste giorno per cui ogni micro-pagamento vale l'overhead. Per la prima volta in trent'anni, l'infrastruttura tecnica e la domanda economica si sono incontrate.

Se gestisci pipeline AI di produzione con agenti che consumano dati, o se hai contenuti proprietari che agenti terzi stanno già scansionando gratis, nel mio hub dedicato all'AI per aziende trovo gli articoli tecnici che uso per orientare i clienti su integrazione agent-to-payment e governance dei costi.

Cosa è cambiato tra il 2023 e oggi: timeline condensata

Sei eventi hanno costruito l'infrastruttura odierna.

DataEventoRilevanza
2023Lightning Labs pubblica L402 (prima versione)HTTP 402 + Lightning + macaroons, protocollo production-grade ma confinato a Bitcoin
maggio 2025Coinbase annuncia x402Protocollo HTTP 402 + USDC su EVM, focus AI agents
1 luglio 2025Cloudflare lancia "Pay Per Crawl" in private betaEdge provider che monetizza crawler AI senza coinvolgere il publisher origin
23 settembre 2025Coinbase e Cloudflare annunciano x402 FoundationGovernance futura neutrale per x402
novembre 2025IETF draft-dhir-http-agent-profile-00Primo tentativo di standardizzare la dichiarazione di agent in HTTP
11 febbraio 2026Lightning Labs rilascia Lightning Agent ToolsSette skill composable, lnget client, Aperture server-side
2 aprile 2026x402 Foundation passa formalmente a Linux FoundationFounding coalition: Stripe, Cloudflare, AWS, Google, Microsoft, Visa, Mastercard, Adyen, Circle
11 marzo 2026L402 diventa bLIP-0026Standardizzazione formale nella Lightning Improvement Proposal repo

Al 23 aprile 2026 i protocolli vivi per HTTP 402 sono tre, con posizionamento diverso. L402 (Bitcoin Lightning) è maturo ma Bitcoin-only, sub-cent, no KYC, macaroons con attenuation/delegation. x402 (EVM+Solana stablecoin) è più giovane ma ha backing enterprise, multi-chain, facilitator-based. Cloudflare Pay Per Crawl è l'approccio pragmatico edge-managed con fatturazione fiat differita per crawler enterprise. Non si escludono a vicenda; stanno convergendo verso un deferred payment scheme che x402 sta formalizzando su proposta Cloudflare.

Cloudflare Pay Per Crawl: la via gestita

Se il tuo sito è già dietro Cloudflare e non vuoi scrivere middleware, la strada meno resistente è Pay Per Crawl, in private beta al 23 aprile 2026. Cloudflare intercetta il crawler AI al livello edge, restituisce 402 con un listino configurabile dal dashboard, accetta il pagamento (stablecoin on-chain o fiat via merchant of record Cloudflare), inoltra la richiesta all'origin solo se il pagamento è valido. Partner publisher noti della beta includono Reddit, Stack Overflow, Condé Nast, O'Reilly, ognuno con strutture di pricing diverse.

Il vantaggio commerciale è che non tocchi l'origin: dal punto di vista della tua applicazione Laravel, la richiesta arriva già pagata o non arriva affatto. Il limite è speculare: non tocchi l'origin, quindi non puoi implementare logiche custom di pricing (sub-tenant diversi, pricing dinamico sul carico LLM dietro, discount su customer pre-autorizzati). Cloudflare ha annunciato al lancio del 2 aprile 2026 una proposta di deferred payment scheme per x402 su cui sta lavorando, che permetterebbe di far accumulare debit per singolo crawler e fatturare una sola volta al giorno via CC: questo risolve il pain dei crawler enterprise che non vogliono gestire wallet on-chain ma accettano fatturazione mensile. Per la PMI italiana media, la strada gestita è quella più sensata: pay-per-crawl toggle, configurazione pricing, stop. Il codice che segue serve quando vuoi o devi implementare on-origin per ragioni di controllo, pricing dinamico o assenza di Cloudflare nel tuo stack.

x402 in dettaglio: come parla un server x402-compliant

Il flusso x402 estende il normale ciclo HTTP con un handshake di pagamento. Sei step.

  1. Client → Server: GET /api/premium (richiesta standard)
  2. Server → Client: HTTP/1.1 402 Payment Required con body JSON contenente istruzioni di pagamento
  3. Client: legge il body, firma un X-PAYMENT payload con la sua chiave privata
  4. Client → Server: retry della stessa richiesta con header X-PAYMENT: <payload base64>
  5. Server → Facilitator: POST /verify con il payload per validazione; POST /settle per broadcasting on-chain
  6. Server → Client: eseguita la risorsa, risposta 200 OK con header X-PAYMENT-RESPONSE contenente conferma settlement

Il body del 402 ha struttura canonica:

{
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "3000",
      "resource": "https://example.com/api/premium",
      "description": "Access to premium weather API",
      "mimeType": "application/json",
      "payTo": "0xYourEVMAddressHere",
      "maxTimeoutSeconds": 60,
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "extra": { "name": "USDC", "version": "2" }
    }
  ]
}

Lo scheme exact trasferisce un amount fisso; una variante upto proposta ma non ancora GA a aprile 2026 permetterebbe pagamenti variabili in base alle risorse consumate (pattern tipico degli LLM che fatturano per token processato). Il maxAmountRequired è in unità minori del token (per USDC con 6 decimali, 3000 = 0,003 USDC). Il campo extra contiene metadata EIP-3009 per il Transfer With Authorization che il client firmerà. Il facilitator di riferimento gratis è api.cdp.coinbase.com/v2/x402 con free tier di 1.000 transazioni al mese, supporto Base, Polygon, Arbitrum, World, Solana.

L402 Lightning: la via Bitcoin

L402 usa una logica diversa. Il server risponde con:

HTTP/1.1 402 Payment Required
WWW-Authenticate: L402 macaroon="AGIAJEemVQUTEyNCR0exk7ek90Cg==",
                        invoice="lnbc1u1p3hvp3tpp5..."

Il client paga l'invoice Lightning, ottiene il preimage (la pre-immagine hash della payment hash), e poi autentica le richieste successive con:

Authorization: L402 <macaroon>:<preimage_hex>

Il macaroon è un token tipo bearer con capability di attenuation: il client può aggiungere caveats (vincoli) senza contattare il server, permettendo delega granulare ai sub-agent. Un agent padre può emettere un macaroon pay-only con cap di 500 sat e passarlo a un worker, senza che il worker possa escalare. Aperture è il reverse proxy gRPC/REST L402-aware di Lightning Labs: lo piazzi davanti al tuo backend e gestisce la negoziazione e la verifica del preimage, la tua applicazione riceve solo request autenticate.

L402 è production-ready da cinque anni sul Lightning Loop. Conviene quando: il tuo traffico è micropayment Bitcoin-native, vuoi zero KYC, ti servono scoped macaroons con delega gerarchica, accetti complessità operativa Lightning (gestire canali, liquidità, fallback).

Implementazione Laravel 12: middleware X402Gate

Non userò SDK esterni perché al 23 aprile 2026 l'ecosystem PHP per x402 è immaturo; scriverò il middleware direttamente contro il facilitator Coinbase. Install minimo:

composer require guzzlehttp/guzzle:^7.9 firebase/php-jwt:^6.10

Middleware:

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

final class X402Gate
{
    // Facilitator endpoint Coinbase CDP (rete testnet Base durante lo sviluppo)
    private const FACILITATOR_URL = 'https://api.cdp.coinbase.com/v2/x402';

    // USDC contract su Base mainnet (6 decimali)
    private const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

    // Versione del protocollo x402 supportata
    private const PROTOCOL_VERSION = 1;

    // Cache TTL per idempotenza: una richiesta pagata vale 10 minuti
    private const IDEMPOTENCY_TTL_SECONDS = 600;

    public function __construct(private readonly Client $httpClient)
    {
    }

    public function handle(Request $request, Closure $next, string $priceCents = '300'): Response
    {
        $paymentHeader = $request->header('X-PAYMENT');

        // Nessun payment: rispondo 402 con istruzioni
        if ($paymentHeader === null || $paymentHeader === '') {
            return $this->buildPaymentRequiredResponse($request, $priceCents);
        }

        // Parse del payload firmato dal client
        $payload = $this->decodePayload($paymentHeader);
        if ($payload === null) {
            return response()->json(['error' => 'Invalid X-PAYMENT payload'], 400);
        }

        // Idempotenza: se questo exact payload e' stato gia' accettato, skip facilitator
        $idempotencyKey = 'x402:' . hash('sha256', $paymentHeader);
        if (Cache::has($idempotencyKey)) {
            return $next($request);
        }

        // Verifico il payload presso il facilitator (firma, network, destinatario)
        try {
            $verifyResponse = $this->httpClient->post(self::FACILITATOR_URL . '/verify', [
                'json' => [
                    'x402Version' => self::PROTOCOL_VERSION,
                    'paymentHeader' => $paymentHeader,
                    'paymentRequirements' => $this->paymentRequirements($request, $priceCents),
                ],
                'timeout' => 8.0,
            ]);
        } catch (GuzzleException $e) {
            Log::warning('x402 verify failed', ['exception' => $e->getMessage()]);

            return response()->json(['error' => 'Payment verification unavailable'], 503);
        }

        $verifyData = json_decode((string) $verifyResponse->getBody(), true);
        if (! ($verifyData['isValid'] ?? false)) {
            return response()->json([
                'error' => 'Payment invalid',
                'reason' => $verifyData['invalidReason'] ?? 'unknown',
            ], 402);
        }

        // Settle on-chain (broadcast della transazione)
        try {
            $settleResponse = $this->httpClient->post(self::FACILITATOR_URL . '/settle', [
                'json' => [
                    'x402Version' => self::PROTOCOL_VERSION,
                    'paymentHeader' => $paymentHeader,
                    'paymentRequirements' => $this->paymentRequirements($request, $priceCents),
                ],
                'timeout' => 15.0,
            ]);
        } catch (GuzzleException $e) {
            Log::error('x402 settle failed', ['exception' => $e->getMessage()]);

            return response()->json(['error' => 'Settlement failed'], 503);
        }

        $settleData = json_decode((string) $settleResponse->getBody(), true);
        if (! ($settleData['success'] ?? false)) {
            return response()->json(['error' => 'Settlement rejected'], 402);
        }

        // Cache idempotenza e proseguo con la richiesta originale
        Cache::put($idempotencyKey, true, self::IDEMPOTENCY_TTL_SECONDS);

        $response = $next($request);
        $response->headers->set('X-PAYMENT-RESPONSE', base64_encode(json_encode($settleData)));

        return $response;
    }

    private function buildPaymentRequiredResponse(Request $request, string $priceCents): Response
    {
        return response()->json([
            'x402Version' => self::PROTOCOL_VERSION,
            'accepts' => [$this->paymentRequirements($request, $priceCents)],
            'error' => 'Payment required',
        ], 402)
            ->header('Content-Type', 'application/json')
            ->header('WWW-Authenticate', 'x402 realm="api"');
    }

    private function paymentRequirements(Request $request, string $priceCents): array
    {
        // Converte cents USDC in unita' minime (6 decimali): 300 cents = 3000000 = $3.00
        // Per 0.003 USDC il priceCents e' "0.3" e la conversione e' 3000
        $amountAtomic = (string) (int) (((float) $priceCents) * 10000);

        return [
            'scheme' => 'exact',
            'network' => config('x402.network', 'base-sepolia'),
            'maxAmountRequired' => $amountAtomic,
            'resource' => $request->fullUrl(),
            'description' => 'Access to ' . $request->path(),
            'mimeType' => 'application/json',
            'payTo' => config('x402.pay_to_address'),
            'maxTimeoutSeconds' => 60,
            'asset' => self::USDC_ADDRESS,
            'extra' => ['name' => 'USDC', 'version' => '2'],
        ];
    }

    private function decodePayload(string $paymentHeader): ?array
    {
        $decoded = base64_decode($paymentHeader, true);
        if ($decoded === false) {
            return null;
        }

        $parsed = json_decode($decoded, true);

        return is_array($parsed) ? $parsed : null;
    }
}

Config file config/x402.php:

<?php

return [
    // Network di default: base-sepolia per test, base per produzione
    'network' => env('X402_NETWORK', 'base-sepolia'),

    // Indirizzo EVM del wallet che riceve i pagamenti
    'pay_to_address' => env('X402_PAY_TO_ADDRESS'),

    // API key del facilitator Coinbase CDP (free tier 1000 tx/month)
    'facilitator_api_key' => env('X402_FACILITATOR_API_KEY'),
];

Applicazione alle route:

Route::middleware(['x402.gate:300'])
    ->get('/api/premium/weather', [WeatherController::class, 'premium']);

Il parametro 300 significa 0,003 USDC per request (3000 in unità minime). Per pricing differenziato tra route, basta passare un valore diverso per ogni gruppo.

Implementazione Symfony 7.2: event subscriber

Stesso pattern tradotto:

<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use App\Service\X402FacilitatorClient;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Attribute\Route;

final class X402GateSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly X402FacilitatorClient $facilitator,
        private readonly CacheItemPoolInterface $cache,
        private readonly LoggerInterface $logger,
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::REQUEST => ['onKernelRequest', 8]];
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();

        // Attivo il gate solo su route marcate con attribute #[X402Paid(...)]
        $x402Config = $request->attributes->get('_x402');
        if ($x402Config === null) {
            return;
        }

        $paymentHeader = $request->headers->get('X-PAYMENT');
        if ($paymentHeader === null) {
            // Status nativo 402 Payment Required disponibile come costante
            $event->setResponse(new JsonResponse([
                'x402Version' => 1,
                'accepts' => [$x402Config->toArray($request)],
                'error' => 'Payment required',
            ], Response::HTTP_PAYMENT_REQUIRED));

            return;
        }

        $idempotencyKey = 'x402_' . hash('sha256', $paymentHeader);
        $item = $this->cache->getItem($idempotencyKey);
        if ($item->isHit()) {
            // Gia' pagata: lascio proseguire
            return;
        }

        // Verify + settle: se fallisce, intercetto con risposta 402
        $verification = $this->facilitator->verifyAndSettle($paymentHeader, $x402Config, $request);
        if (! $verification->ok) {
            $this->logger->warning('x402 verification failed', ['reason' => $verification->reason]);
            $event->setResponse(new JsonResponse(
                ['error' => 'Payment invalid', 'reason' => $verification->reason],
                Response::HTTP_PAYMENT_REQUIRED,
            ));

            return;
        }

        // Cache idempotenza e salvo la risposta settlement per il response listener
        $item->set($verification->settlementPayload);
        $item->expiresAfter(600);
        $this->cache->save($item);

        $request->attributes->set('_x402_settlement', $verification->settlementPayload);
    }
}

Nel controller usi un attribute custom:

#[Route('/api/premium/weather', methods: ['GET'])]
#[X402Paid(amount: '3000', description: 'Premium weather access')]
public function premiumWeather(): JsonResponse
{
    return new JsonResponse(['temp' => 22.4, 'city' => 'Torino']);
}

L'attribute X402Paid popola _x402 nelle request attributes durante il routing; il subscriber lo legge e applica il gate. Questo evita di modificare controller con logic di payment, mantenendo la responsabilità single-purpose.

Implementazione PHP vanilla: front controller

Per applicazioni senza framework, lo schema è identico ma con interface minima.

composer require guzzlehttp/guzzle:^7.9

Helper lib/X402Gate.php:

<?php

declare(strict_types=1);

namespace App\Lib;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

final class X402Gate
{
    private const FACILITATOR_URL = 'https://api.cdp.coinbase.com/v2/x402';
    private const USDC_BASE_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

    public function __construct(
        private readonly Client $http,
        private readonly string $network,
        private readonly string $payToAddress,
        private readonly string $facilitatorApiKey,
    ) {
    }

    public function gate(string $resourceUrl, string $amountAtomic, ?string $paymentHeader): GateResult
    {
        // Primo contatto: nessun X-PAYMENT
        if ($paymentHeader === null || $paymentHeader === '') {
            return GateResult::paymentRequired(
                $this->paymentRequirements($resourceUrl, $amountAtomic)
            );
        }

        // Verify: la firma e' corretta, l'amount e' sufficiente, il destinatario e' corretto?
        $verify = $this->callFacilitator('/verify', $paymentHeader, $resourceUrl, $amountAtomic);
        if ($verify === null || ! ($verify['isValid'] ?? false)) {
            return GateResult::rejected($verify['invalidReason'] ?? 'verification_failed');
        }

        // Settle: broadcast on-chain della transazione firmata
        $settle = $this->callFacilitator('/settle', $paymentHeader, $resourceUrl, $amountAtomic);
        if ($settle === null || ! ($settle['success'] ?? false)) {
            return GateResult::rejected('settlement_failed');
        }

        return GateResult::accepted(base64_encode(json_encode($settle)));
    }

    private function callFacilitator(string $path, string $paymentHeader, string $resourceUrl, string $amountAtomic): ?array
    {
        try {
            $response = $this->http->post(self::FACILITATOR_URL . $path, [
                'headers' => ['Authorization' => 'Bearer ' . $this->facilitatorApiKey],
                'json' => [
                    'x402Version' => 1,
                    'paymentHeader' => $paymentHeader,
                    'paymentRequirements' => $this->paymentRequirements($resourceUrl, $amountAtomic),
                ],
                'timeout' => 10.0,
            ]);

            return json_decode((string) $response->getBody(), true);
        } catch (GuzzleException) {
            return null;
        }
    }

    private function paymentRequirements(string $resourceUrl, string $amountAtomic): array
    {
        return [
            'scheme' => 'exact',
            'network' => $this->network,
            'maxAmountRequired' => $amountAtomic,
            'resource' => $resourceUrl,
            'description' => 'Paid API access',
            'mimeType' => 'application/json',
            'payTo' => $this->payToAddress,
            'maxTimeoutSeconds' => 60,
            'asset' => self::USDC_BASE_ADDRESS,
            'extra' => ['name' => 'USDC', 'version' => '2'],
        ];
    }
}

final class GateResult
{
    public function __construct(
        public readonly string $status,
        public readonly ?array $paymentRequirements = null,
        public readonly ?string $rejectionReason = null,
        public readonly ?string $settlementHeader = null,
    ) {
    }

    public static function paymentRequired(array $requirements): self
    {
        return new self('payment_required', paymentRequirements: $requirements);
    }

    public static function rejected(string $reason): self
    {
        return new self('rejected', rejectionReason: $reason);
    }

    public static function accepted(string $settlementHeader): self
    {
        return new self('accepted', settlementHeader: $settlementHeader);
    }
}

Front controller public/api.php:

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use App\Lib\X402Gate;
use GuzzleHttp\Client;

$gate = new X402Gate(
    http: new Client(),
    network: $_ENV['X402_NETWORK'] ?? 'base-sepolia',
    payToAddress: $_ENV['X402_PAY_TO_ADDRESS'] ?? '',
    facilitatorApiKey: $_ENV['X402_FACILITATOR_API_KEY'] ?? '',
);

$resourceUrl = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$paymentHeader = $_SERVER['HTTP_X_PAYMENT'] ?? null;

$result = $gate->gate($resourceUrl, amountAtomic: '3000', paymentHeader: $paymentHeader);

match ($result->status) {
    'payment_required' => renderPaymentRequired($result->paymentRequirements),
    'rejected' => renderRejected($result->rejectionReason),
    'accepted' => renderResource($result->settlementHeader),
};

function renderPaymentRequired(array $requirements): void
{
    http_response_code(402);
    header('Content-Type: application/json');
    header('WWW-Authenticate: x402 realm="api"');
    echo json_encode([
        'x402Version' => 1,
        'accepts' => [$requirements],
        'error' => 'Payment required',
    ]);
}

function renderRejected(string $reason): void
{
    http_response_code(402);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Payment invalid', 'reason' => $reason]);
}

function renderResource(?string $settlementHeader): void
{
    http_response_code(200);
    header('Content-Type: application/json');
    if ($settlementHeader !== null) {
        header('X-PAYMENT-RESPONSE: ' . $settlementHeader);
    }

    // Contenuto protetto
    echo json_encode(['temp' => 22.4, 'city' => 'Torino', 'timestamp' => time()]);
}

Test con curl da un agent simulato (il payload reale va firmato con eth_signTypedData):

# Primo colpo: 402
curl -i https://api.example.test/api.php
# HTTP/1.1 402 Payment Required
# WWW-Authenticate: x402 realm="api"
# {"x402Version":1,"accepts":[{...}],"error":"Payment required"}

# Secondo colpo con payload firmato
curl -i -H 'X-PAYMENT: eyJzaWduYXR1cmUi...' https://api.example.test/api.php
# HTTP/1.1 200 OK
# X-PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlLC...
# {"temp":22.4,...}

Numeri veri Q1 2026 vs hype

Il 11 marzo 2026 CoinDesk ha pubblicato un'analisi on-chain critica che vale la pena citare testualmente. Volume giornaliero reale x402: 28.000 dollari. Transazioni giornaliere: 131.000. Pagamento medio: 0,20 dollari. Drop del 92% rispetto al picco di 731.000 transazioni giornaliere di dicembre 2025. Volume cumulativo: 119 milioni di transazioni su Base, 35 milioni su Solana, 600 milioni di USD annualized. Artemis, analista terzo, stima che circa metà delle transazioni osservate siano "gamified", cioè testing e incentive-farming, non commercio reale.

Questo non è pessimismo: è un dato utile. Significa che sei in una fase in cui l'infrastruttura tecnica è pronta, la governance è passata a un soggetto neutrale (Linux Foundation), i big backer sono allineati, ma la domanda reale di mercato è ancora prevalentemente sviluppatori che fanno demo. Se la tua strategia di business dipende da incasso immediato da HTTP 402, stai costruendo su sabbia. Se invece implementi HTTP 402 come opzione architetturale disponibile per quando la domanda si materializzerà, stai preparando il terreno senza bruciare runway.

Stack Overflow ha pubblicato un retrospective il 19 febbraio 2026 sui primi sei mesi di pay-per-crawl. I numeri assoluti non sono rivelati, ma il framing è chiaro: per il momento è un canale di signaling ("il contenuto Stack Overflow ha valore, pagatelo"), non un revenue stream significativo. Lo stesso framing vale per Reddit e O'Reilly.

Quando NON ha senso attivare HTTP 402

Cinque scenari dove il pattern non conviene.

Primo: domain authority bassa. Se il tuo sito non è ancora destinazione preferenziale dei crawler AI, non stai risolvendo un problema; stai aggiungendo frizione a traffico che forse arriverà, forse no.

Secondo: audience non tecnica. I tuoi utenti sono umani che leggono articoli business con browser normale. X-PAYMENT non lo mandano. L'unica cosa che ottieni è complessità operativa per il tuo team.

Terzo: blog piccolo (sotto 50.000 pageview/mese). Il break-even tra costo di integrazione, gestione facilitator API, hardening del wallet e incasso non è raggiungibile. Il tempo che ci spendi vale più del ricavo.

Quarto: contenuti che non alimentano training AI. Articoli evergreen tecnici di nicchia, documentazione interna, landing page commerciali. I crawler AI non hanno interesse a scansionarli a pagamento.

Quinto: non hai ancora content negotiation text/markdown. Se stai servendo HTML a Claude Code, l'agent paga 4x il costo di token per processare la stessa pagina. Prima di chiedere soldi, convertirlo in Markdown è un favore reciproco: lui paga meno per estrarre significato, tu incassi un prezzo equo perché ha senso. Ne ho scritto in dettaglio nel pezzo di ieri sulla content negotiation.

Dove siamo e dove andiamo

HTTP 402 nel 2026 è un protocollo tecnicamente maturo con backer eccellenti e volume reale modesto. Non è il momento di costruire un business case che dipende esclusivamente da esso, ma è il momento in cui non implementarlo affatto cessa di essere neutrale: ogni mese di ritardo è un mese in cui un competitor con la stessa integrazione si posiziona prima. La proposta pragmatica che faccio ai clienti PMI è tenere il middleware disabilitato ma deployato in staging, integration-tested, con un toggle .env per attivarlo. Quando la domanda reale si materializza (probabilmente quando uno dei grandi LLM provider integrerà wallet automatico di default, e quando McKinsey smetterà di proiettare e inizieremo a vedere i numeri), flippare il toggle è questione di minuti, non di mesi di sviluppo.

La narrativa attorno a x402 e L402 è che stanno "resuscitando HTTP 402 dopo ventinove anni". Il framing più onesto è che stanno finalmente fornendo l'infrastruttura economica che HTTP 402 aveva sempre promesso senza poter mantenere. Il fatto che Cloudflare, Coinbase, Stripe, Visa, Mastercard, AWS, Google e Microsoft siedano tutti nel fondo della x402 Foundation sotto Linux Foundation non è un dato casuale: è la scommessa simultanea dei big player su un'infrastruttura che sarà presente nel web dei prossimi dieci anni, indipendentemente da quale protocollo prevale o da quale volume transazionale si consolida. Essere early adopter senza essere fragile è la posizione difendibile.

Un'osservazione finale sui tre path implementativi che ho mostrato. Laravel 12 è la strada preferita quando il tuo stack è già Laravel e vuoi il pagamento come una capability di middleware trasversale, applicabile per gruppo di route con un singolo alias. Symfony 7.2 ha il vantaggio idiomatico del Response::HTTP_PAYMENT_REQUIRED costante nativa e dell'attribute system che mantiene il payment gate dichiarativo sul controller, senza sporcare la business logic. PHP vanilla è l'opzione che scelgo quando monetizzo un singolo endpoint in un progetto legacy senza framework, o quando voglio tenere il gate x402 completamente separato come proxy davanti a un backend che non sa niente di payment: lo deployo come micro-servizio, lo pongo davanti, il servizio interno resta invariato. Nella mia pipeline personale uso un mix: Laravel per route applicative, proxy PHP vanilla su un paio di endpoint di data export che servivo gratis e che ora voglio rendere opzionalmente pagati senza modificare il core. Tre stili, stessa semantica, stessa compatibilità x402.

Se stai pianificando integrazione HTTP 402 su un tuo progetto PMI, su un SaaS che espone API premium o su un portale di contenuti tecnici, e vuoi capire se ha davvero senso economico nel tuo caso specifico o se è prematuro, il modulo di preventivo gratuito risponde in due minuti se il tuo scenario rientra nel mio perimetro. Sette domande, nessun impegno. Prima di scrivere middleware x402, è meglio sapere se il middleware va scritto.

Ultima modifica: