Skip to content

Pricing Analytics

Observe mode: measuring what agents would pay before you charge

By Requestway · · 6 min read

Observe mode lets you put x402 prices on your routes without charging anyone: every request is served as usual, and every charge that would have happened is recorded. After a week or two you know which routes agents call, how often, and what they would have earned at the price you picked, before a single 402 reaches a client. This article covers how observe mode works in Laravel, how to get the same data in Node, and how to read the numbers without fooling yourself.

What observe mode does, and what it does not

In enforce mode, an unpaid request to a paid route gets 402 Payment Required, and the client has to sign a payment and retry. In observe mode, the same request is served straight away. Nothing is charged, and the client is never asked to sign anything.

What you get instead is a record of the price each request would have paid. In the requestway/laravel-x402 package, each would-be charge:

  • is written to the log,
  • is recorded in the x402_payments table with status = observed,
  • fires a PaymentObserved event,
  • and adds a header to the response: X-X402-Would-Charge: $0.25.

What observe mode does not tell you is whether a caller would actually pay. Nobody had to make that decision. Keep that in mind when you read the results; there is a section on it below.

The package has three modes, set with X402_MODE:

Mode Unpaid request Recorded
enforce (default) 402 Payment Required Settled and failed payments
observe Served normally, with X-X402-Would-Charge Would-be charges, status = observed
off Served normally Nothing; the package is disabled

Turning on observe mode in Laravel

Declare prices exactly as you would for enforcement. The route definitions do not change between modes, which is the point: when you switch to enforce, you charge exactly what you measured.

Route::get('/market-data', MarketDataController::class)->middleware('x402:0.01');
Route::get('/report', ReportController::class)->middleware('x402:premium');

Then set the mode:

X402_MODE=observe

Run the migration if you have not already, so observed requests land in the table:

php artisan migrate

Call a route and you will see the header:

HTTP/1.1 200 OK
Content-Type: application/json
X-X402-Would-Charge: $0.01

The header is visible to clients as well as to you. That is useful: an agent developer looking at responses can see that the route has a price and what it will be, before it is enforced.

Keeping the table small

On a busy endpoint, one row per request adds up. Set X402_RECORD_OBSERVED=false to keep the PaymentObserved events and log lines without writing table rows. If you still want stored data at lower volume, sample inside a PaymentObserved listener instead:

Event::listen(PaymentObserved::class, function (PaymentObserved $event): void {
    if (random_int(1, 10) !== 1) {
        return; // keep roughly one in ten
    }

    Log::info('x402 observed', [
        'route' => $event->context->route->label(),
        'would_charge' => $event->context->amountUsd(),
    ]);
});

If you sample, multiply counts back up when you report them, and say so in whatever you show to others.

Reading the numbers

With rows in the table, the X402Payment model has an observed() scope:

use Requestway\X402\Models\X402Payment;

X402Payment::query()
    ->observed()
    ->between(now()->subWeek(), now())
    ->get()
    ->groupBy('route')
    ->map(fn ($rows) => $rows->count().' requests, $'.$rows->sum('amount_usd').' foregone');

That gives you, per route, the request count and the revenue you would have collected at the configured price. Each row also has the method, resource URL, network and timestamp, so you can look at demand by hour or day as well.

Three questions to ask of the data:

  1. Which routes get called at all? A route with almost no observed traffic will earn almost nothing at any price. That is useful to know before you build billing around it.
  2. Is demand steady or bursty? A route hit thousands of times in one hour and then never again is probably one client in a loop, not a market.
  3. How does the would-be revenue compare to what the route costs you to serve? Observe mode reports the price you set, so you can compare it with your own cost per request and see whether the price covers it.

For pricing strategy itself, pricing an API for AI agents and what to charge AI agents for go deeper.

Observe mode in Node

The official Node packages (@x402/express, @x402/next, @x402/hono) enforce payment on the routes you configure. To measure first, leave the payment middleware off the route and record what it would have charged yourself. @requestway/reporter has an observed() method for exactly this:

import { randomUUID } from 'node:crypto'
import express from 'express'
import { reporter } from '@requestway/reporter'

const rw = reporter({ key: process.env.REQUESTWAY_KEY })
const app = express()

app.use('/premium-data', (req, res, next) => {
  rw.observed({
    route: 'GET /premium-data',
    method: req.method,
    resource: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
    mode: 'observe',
    priceUsd: '0.01',
    amountAtomic: '10000',
    asset: 'USDC',
    network: 'eip155:8453',
    payTo: '0xYourAddress',
    requestId: randomUUID(),
  })
  next()
})

app.get('/premium-data', (req, res) => res.json({ price: 42 }))

A few details matter here:

  • priceUsd and amountAtomic are strings. USDC has 6 decimals, so $0.01 is 10000 atomic units.
  • observed() is synchronous, returns nothing and never throws, so it cannot break the request. Without a key it does nothing.
  • Query strings are stripped from resource before the event is buffered, so API keys or emails in a URL do not leave your app.
  • On serverless platforms, pass waitUntil to reporter() or await rw.flush(); otherwise buffered events are lost when the function freezes.

When you are ready to charge, replace this middleware with paymentMiddleware and the same price. The Express, Next.js and Hono guide shows that setup.

What observed demand can and cannot tell you

Observed numbers are an upper bound on paid demand, not a forecast. Every caller in observe mode got the response for free. Some of them will pay when asked; some will not; some cannot, because they have no wallet or no funds.

A worked example, with made-up numbers: a route priced at $0.01 shows 50,000 observed requests in a week, so $500 of would-be revenue. If most of that traffic comes from callers that cannot pay, enforced revenue could be a small fraction of $500. If it comes from agents built to pay x402 routes, it could be close. Observe mode cannot tell those apart for you; your logs and your knowledge of who calls the API can.

Other things that distort the numbers:

  • Your own traffic. Health checks and internal callers inflate counts. In enforce mode you would exempt them with bypass rules (X402_BYPASS_IPS, API keys in X402_API_KEY_HEADER, or free-for-auth for signed-in users), so filter them out of your analysis the same way.
  • Retries and loops. A single misbehaving client can dominate a week of data. Look at the distribution, not just the total.
  • Price sensitivity. Observe mode measures demand at zero cost. It says nothing about how demand changes between $0.01 and $0.10. If you want to compare prices, change one route's price and observe again, rather than guessing.

From observe to enforce

The package's mainnet checklist suggests a week in observe mode on mainnet configuration before you charge. That way the switch to enforce changes one variable:

  1. Configure for mainnet: X402_NETWORK=eip155:8453, your mainnet X402_PAY_TO (check every character), a production facilitator.
  2. Set X402_MODE=observe and run php artisan x402:doctor.
  3. Watch observed demand for a week. Adjust prices or drop routes nobody calls.
  4. Run php artisan x402:test against each route, including --underpay and --fail-settlement.
  5. Set X402_MODE=enforce and make one small real payment end to end.

After the switch, compare settled revenue per route with what you observed. The gap is the share of callers who would not or could not pay. The testnet to mainnet checklist covers the rest of the production setup.

Seeing observed demand in Requestway

If you would rather not maintain the queries yourself, Requestway shows observed demand as would-be revenue per route, next to settled and failed counts once you enforce. In Laravel, create a project and set X402_PLATFORM_KEY; observe events are reported along with payments, from a queued job after the response. In Node, use rw.observed() as above. The install guide has both setups. Reporting never blocks or fails a request.

FAQ

Does x402 observe mode charge anything?

No. Requests are served as normal, no payment is requested or verified, and each would-be charge is only recorded, with an X-X402-Would-Charge header on the response.

How do I turn on observe mode in laravel-x402?

Set X402_MODE=observe in .env. Keep your existing price middleware on the routes; observed requests are recorded with status = observed and fire PaymentObserved.

Can I use observe mode without storing a row for every request?

Yes. Set X402_RECORD_OBSERVED=false to keep the events and log lines without table rows, or sample inside a PaymentObserved listener.

Is there an observe mode in @x402/express?

Not as a switch in the setup described here. Leave the payment middleware off the route and record each would-be charge with rw.observed() from @requestway/reporter, then add paymentMiddleware when you are ready to charge.

What is the difference between X402_MODE=observe and X402_MODE=off?

observe serves requests free but records what each would have been charged. off disables the package entirely: protected routes behave as if they were never protected and nothing is recorded.