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_paymentstable withstatus = observed, - fires a
PaymentObservedevent, - 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:
- 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.
- 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.
- 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:
priceUsdandamountAtomicare strings. USDC has 6 decimals, so$0.01is10000atomic 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
resourcebefore the event is buffered, so API keys or emails in a URL do not leave your app. - On serverless platforms, pass
waitUntiltoreporter()orawait 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 inX402_API_KEY_HEADER, orfree-for-authfor 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:
- Configure for mainnet:
X402_NETWORK=eip155:8453, your mainnetX402_PAY_TO(check every character), a production facilitator. - Set
X402_MODE=observeand runphp artisan x402:doctor. - Watch observed demand for a week. Adjust prices or drop routes nobody calls.
- Run
php artisan x402:testagainst each route, including--underpayand--fail-settlement. - Set
X402_MODE=enforceand 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.
Related articles
-
Pricing
Pricing an API for AI agents: per-request pricing explained
7 min read
-
Strategy
What to charge AI agents for and which endpoints to put behind x402
7 min read
-
Production
A checklist for moving an x402 API from testnet to mainnet
7 min read