Getting started
Ten minutes, four steps. At the end you'll have a running ledger of every agent that crossed your app, attributed to a cryptographic identity, and exportable as something you can invoice from.
1 Install the middleware
Zero dependencies. It uses Node's own crypto and pulls nothing into your tree.
npm install wayleave
Requires Node 18 or newer.
2 Get an API key
Go to Account → Issue key. Give it a
label like production. The key is shown once and never
again — we store only a hash of it, so if you lose it you issue a new one
rather than recovering the old.
Give staging its own key. If one leaks you can revoke it without stopping your production billing.
3 Start in observe-only mode
Do not price anything yet. Run it for a few days with no prices and no deny rules, and look at what actually crosses your app. Almost everyone is surprised, and you cannot price sensibly until you know what is out there.
import Wayleave from 'wayleave';
const gate = new Wayleave({
pricedPaths: {}, // observe only — nothing is charged, nothing is blocked
rules: {}, // no deny rules — measure first, police later
// Builds the buffering, retrying sink for you.
meter: { apiKey: process.env.WAYLEAVE_METER_KEY },
});
app.use(gate.express()); // mount BEFORE your auth middleware
process.on('SIGTERM', () => gate.sink.close()); // drain on shutdown
Mount it before authentication. After your auth layer you only see traffic that already had a key, which is exactly the traffic you do not need to measure.
Always call sink.close() on shutdown.
Without it, whatever is buffered when your container cycles is data you
never receive.
curl -i -A 'GPTBot/1.0' https://your-app.com/health # expect 200
If this integration ever returns 402 or 403,
it is misconfigured. In observe-only mode nothing should ever be refused.
4 Read your ledger
Within a minute, crossings appear on your ledger. Four numbers matter:
- Unbilled agent crossings
- Agents that crossed and produced no revenue. This is the big number, and it is the point: it is what you are currently giving away.
- Turned away at a 402
- Agents that asked for a priced route and never paid. Most crawlers have no wallet today, so they are refused rather than served free. You are protected, not paid — and the ledger shows exactly how much demand is standing at your gate.
- Collectable
- Money a payment rail actually confirmed. Invoice this number and no other. It will be small at first. That is honest, not broken.
- Unconfirmed
-
Your middleware booked it as billed, but no rail confirmed it. Usually
settlement lag. If it stays high, your
verifyPaymentis trusting something it should not — do not invoice it.
Only verified_agent rows are marked invoiceable. A
self-declared bot is an identity you cannot defend on an invoice, so we
show it as observed only.
5 When you're ready, charge
Two changes. Both matter.
import { coinbaseFacilitator } from 'wayleave/x402';
const gate = new Wayleave({
pricedPaths: { '/api/premium': 0.05 }, // 5¢ per crossing
// REQUIRED on anything priced. Without it, a bot that sends a browser
// user-agent and an accept-language header is classified as human and
// crosses free — that is two headers, and it is the whole bypass.
strictPricedPaths: true,
confirmHuman: req => Boolean(req.session?.userId), // your session, never a header
// REQUIRED to sell anything. Defaults to deny: configure nothing and
// priced routes stay 402 forever, deliberately.
verifyPayment: coinbaseFacilitator({
apiKeyId: process.env.CDP_API_KEY_ID,
apiKeySecret: process.env.CDP_API_KEY_SECRET,
receivingAddress: process.env.WAYLEAVE_RECEIVING_ADDRESS,
}),
sink,
});
app.use(gate.express()); // async — it awaits the facilitator
gate.express() or gate.handleAsync().
Confirming settlement is a network call, so verifyPayment
returns a Promise. The synchronous handle() cannot await one
— it will tell you so rather than silently denying every payment, which
is what versions before 0.3.0 did.
Set your receiving address too. Agents pay a facilitator and the facilitator pays that address. Wayleave never holds your funds and is never a step in the path — we record that money moved, we do not move it.
6 Tell someone it happened
Connect your own Sendable account under Integrations, decide which of your app's events should send a message, and post the event when it happens. Wayleave does not write the copy or keep a template — Sendable does that, under your account and your billing.
curl -X POST https://meter.wayleave.dev/v1/notify \
-H "authorization: Bearer $WAYLEAVE_API_KEY" \
-H "content-type: application/json" \
-d '{"event":"lead_created",
"recipient":"agent@example.com",
"data":{"property_address":"123 Main St","lead_name":"John Smith"}}'
- Your API key decides who you are. There is no customer id in the body, deliberately — otherwise one app could spend another account's quota and send mail in their name.
-
An event with no rule sends nothing, and says so. You
get
{"sent":false,"skipped":true}rather than an error, so your app does not have to know what the dashboard is configured for. Skipped events still appear in Delivery activity — which is the first place to look when no message arrives. -
recipientis optional if the event has a default address. Passing one overrides it. -
datamust be flat. Strings, numbers and booleans. A nested object is refused here with the field named, rather than further along where the error is harder to read. - Your key is encrypted and cannot be read back — not by us through the dashboard, and not by you. Replace it any time; your event rules survive the change.
What this does not do
Worth knowing before you rely on it.
- It is a tollbooth, not a wall. Only the verified lane is cryptographic. A bot willing to lie about being a browser gets through, and there is no TLS fingerprinting here — that belongs at your CDN. Wayleave prices disclosure; it does not detect concealment.
- It works because good operators want to be identified. Signed traffic gets allowlisted instead of blocked. That is the bet.
- Key rotation is your job for now. You can pass a resolver function for keys, but there is no JWKS fetcher in the box.
-
Rate-limit and replay state is per-process unless you
supply a shared
store. Two instances mean two independent limiters.
If something looks wrong
- No crossings appear
-
Check the key is right (
401in your logs viaonError), that you mounted before auth, and that at least one flush interval has passed. The sink batches; it is not instant. - Numbers look doubled
- They should not be. Every event carries an idempotency key and the meter refuses repeats, so retries are safe — send the same batch as many times as you like. If you genuinely see doubles, tell us; that is a bug in us, not in you.
- Collectable is zero but agents are paying
-
You have no receiving address set, or your
verifyPaymentis not wired to a real facilitator. We only count settlement a rail confirmed. - Agents pay but still get 402
-
Almost always wayleave older than 0.3.0, or calling the synchronous
handle()with a real facilitator. Settlement is a network call, so your verifier returns a Promise and the sync path cannot await it. Upgrade and usegate.express(). On 0.3.0 the rejection reason says this outright. - Real users are getting 402
-
You turned on
strictPricedPathswithout a workingconfirmHuman. It fails closed on purpose. Point it at your session, never at a header.