Metering & verification
Verify gateway requests and report exact dynamic usage through the right metering channel.
Install the backend SDK in a Node 22 or newer application:
pnpm add @farthershore/backend
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv();
initFromEnv() reads FS_RUNTIME_TOKEN and lazily bootstraps on first use. In a
normal deployment you do not set a Core URL; the token determines the business,
backend, environment scope, verification keys, routes, transport, and metering
configuration.
Verify before parsing or handling
Farther Shore signs the exact raw body hash. In Express, capture raw bytes before the SDK middleware, then parse JSON after verification:
app.get("/healthz", (_req, res) => res.json({ ok: true }));
app.use(express.raw({ type: shouldCaptureRawBody, limit: "10mb" }));
app.use((req, _res, next) => {
if (Buffer.isBuffer(req.body) && req.body.length > 0) {
(req as typeof req & { rawBody?: Buffer }).rawBody = req.body;
}
next();
});
app.use(fs.middleware());
app.use(parseVerifiedJson);
Use the generated Node template for shouldCaptureRawBody and
parseVerifiedJson; it shares the platform's streaming content-type and body
size contract instead of hard-coding its own version.
Strict middleware is fail-closed by default. It verifies the request signature
and signed context, attaches req.fartherShore, and strips every inbound
x-fs-* header before your handler runs. Never read identity from an ordinary
header.
fs.handler() is the ergonomic verified-handler boundary:
import { requireMember, requirePermission } from "@farthershore/backend";
app.post(
"/v1/jobs",
fs.handler(async (ctx, req, res) => {
const { memberId } = requireMember(ctx);
requirePermission(ctx, "jobs:create");
res.status(201).json({ memberId });
}),
);
The gateway is still the route-level authorization boundary. Backend helpers are useful for narrowing the verified subject and for finer record- or field-level checks.
Choose the correct metering channel
There is one verb — ctx.report({ meter, values, dims?, quote? }) — and the
SDK chooses the transport from the response adapter and when you call it:
| Moment | Transport | Network call from handler | Effect |
|---|---|---|---|
| Structural request count | fs.requests() in the business contract | None | Counted by the gateway; never reported by the backend. |
| Before the response is sent | signed in-band x-fs-metering headers | None | Settles the request's monetary reservation on the way out. |
| After the response is on the wire (streams) | attested post-stream channel | One callback | Rated under the request's served identity; billing-only. |
| Deferred work for the original served request | post-stream channel with its retained verified context | One callback total per served request | Rated under that request's served identity; billing-only, not independent cron reporting. |
Do not report requests from backend code. Backends report measurements,
never money: the platform owns what a measurement costs.
Express middleware attaches the response adapter automatically. A
framework-neutral verifyRequest() call needs a responseSink to stamp
in-band headers; without one it uses post-stream delivery even if the handler
has not returned. See the Fetch adapter example.
Report from the verified context
ctx.report() is the one reporting verb. meter is the meter key declared
with fs.meter() in the business; values are keyed by measure key
(fs.measure()); dims are optional catalog dimension selectors keyed by
dimension key (fs.dimension()). Identity rides the verified context — there
is no subscription or request id argument to forget, and the served identity
(subscription, commercial release, rating context) is stamped by the gateway
inside the signed usage event.
app.post(
"/v1/complete",
fs.handler(async (ctx, req, res) => {
const result = await model.complete(req.body.prompt);
await ctx.report({
meter: "model_usage",
values: {
input_tokens: result.inputTokens,
output_tokens: result.outputTokens,
},
dims: { model: "acme-4" },
});
res.json({ text: result.text });
}),
);
The meter, measure, and dimension keys are plain strings validated by the gateway against the served release's measurement schema; an unknown key is rejected loudly, never silently dropped. Values must be non-negative finite numbers.
Transport is automatic. Before the response is sent, the measurement rides
signed x-fs-metering headers — the gateway removes and verifies them, then
settles the request using the served plan and policy, with no extra network
call. A malformed report throws; a delivery fault resolves { ok: false } so a
metering hiccup never breaks your endpoint. For a pricing rule declared
backendQuoted, an optional quote: { currency, amountNanos } field carries a
proposed, non-negative rate input that the platform clamps; backends never
report money otherwise.
Always inspect the result, record delivery failures with request correlation,
and investigate missing usage. ok: true with transport: "in_band" proves
headers were attached, not that the gateway accepted or settled the report.
Do not blindly report the same usage again: the reporting verb is not an
application-level deduplication key.
For a quote, amountNanos is the proposed per-unit rate, not the total cost.
Prefer a decimal integer string so large exact amounts survive JavaScript.
When the gateway cannot trust a report
Your response is never rewritten because of a metering report. By the time the gateway inspects the signed headers your endpoint has already run and committed its work, so a report the gateway cannot verify is discarded, not escalated into an error for your caller. The request is then settled on the route's declared or fixed costs — the same settlement a route with no in-band report at all receives.
A discarded report is visible on the response itself:
HTTP/1.1 201 Created
x-fs-metering-report: rejected;reason=token_backend_mismatch
The header appears only when a report was rejected; an accepted report leaves it absent. The reason names the exact check that failed — it never contains a token, hash, or identifier:
reason | What it means |
|---|---|
token_not_published | The runtime token is not present in edge state. Newly minted tokens take a moment to propagate; a token for an environment that is not live never propagates at all. |
token_revoked | The token exists at the edge but is revoked. |
token_kind_mismatch | The token's fsrt_live_ / fsrt_test_ prefix disagrees with the kind it was minted as. |
token_business_mismatch | The token belongs to a different business than the request's credential. |
token_missing_metering_operation | The token was minted without the metering operation. |
token_environment_mismatch | The token is scoped to one environment and the request authenticated into another. |
token_backend_mismatch | The token is bound to a specific backend and the request resolved to a different one — commonly an environment override row versus the production row of the same slug. |
token_hash_mismatch | The published record does not match the presented token. Report this. |
signature_mismatch | The signature did not verify against the presented token. |
request_context_mismatch | The report's method/path do not match the request the gateway served. Most often the backend sits behind an origin path prefix, so it observes a different path than the caller sent. |
partial_headers | Some but not all of the three metering headers arrived — usually a proxy stripping headers. |
invalid_measurements | The measurement lane was present but malformed. |
parse_or_verify_error | The payload could not be parsed or verified at all. |
Every rejection also emits a metering_report_invalid warning carrying the same
reason alongside the request id, so support can correlate it.
Reporting a meter that the route does not declare, or that the runtime token is
not scoped to, is a different and stricter case: that is a scope violation
rather than an untrusted report, and it still returns 400 metering_report_not_allowed naming the offending routes or meters.
Post-stream usage
Declare a streaming binding explicitly, with a finite settlement maximum when the plan can run out of money:
const stream = fs.route("/v1/stream", { post: { backend: api } });
fs.meterRoutes("stream-model-usage", stream, {
reports: [modelUsage],
postStream: { settlementMax: [outputTokens.atMost(8192)] },
});
After the stream closes, call the same verb on the verified request context:
await ctx.report({
meter: "model_usage",
values: { output_tokens: finalOutputTokens },
});
Once the response is on the wire, ctx.report() automatically switches to the
attested post-stream channel, carrying the same served identity that rode the
signed request context. Post-stream reports write the billable measurement
but do not retroactively change real-time enforcement windows.
Delivery is asynchronous and order-independent
The platform's own record of the served request — the gateway receipt — reaches the billing plane asynchronously. A fast background job routinely finishes and reports before its receipt lands. That is expected, and it is not your problem to handle:
- A verified report that arrives ahead of its receipt is accepted (
202) and held, then settled automatically once the receipt arrives. It is not rejected and it is not lost. - Settlement is exactly once. Redelivering the identical report is a no-op; reporting different measurements under the same served request is rejected, because the first accepted report is that request's economic identity.
- A held report is bounded at 24 hours. In the (platform-fault) case where the receipt never arrives, it is dropped with an internal billing-divergence alert rather than silently.
So await ctx.report(...) resolving { ok: true } means the measurement is
durably accepted, not necessarily already rated. There is nothing to poll.
Reading a failed delivery
Delivery faults resolve { ok: false } (they never throw — a metering hiccup
must not break your endpoint) and carry the platform's own diagnosis, so you do
not need to wrap fetch to see why:
const result = await ctx.report({ meter: "pages", values: { pages } });
if (!result.ok) {
logger.warn(
{ code: result.code, status: result.status, message: result.message },
result.reason,
);
}
code and message are the platform's error code and message when the failure
came back in a response body, and status is the HTTP status of the final
attempt; all three are absent for local validation faults and transport errors,
where reason alone describes the problem. The SDK has already retried
everything worth retrying by the time you see ok: false.
Choose one transport for the entire served request. If any report already
stamped in-band headers, later post-stream reporting fails with ok: false.
For streaming work, collect the final measurements and send one final batch;
do not send an input-token report in-band followed by output tokens post-stream.
A served request owns exactly one post-stream callback, so report every
post-response meter in one call using the array form — sequential
single-meter calls after the first flush resolve { ok: false } and that
usage is not delivered:
await ctx.report([
{ meter: "model_usage", values: { output_tokens: finalOutputTokens } },
{ meter: "jobs", values: { jobs: 1 } },
]);
All entries of a batch share one quote (two different quotes throw) and one
dims tuple — the request receipt rates under (route, dims), so report each
dims tuple on its own request. The same one-quote / one-dims rule applies to
in-band accumulation before the response is sent. For bounded
streaming — the gateway clamping the client's max_tokens up front — declare
maxOutputUnits on the binding instead; see
Monetary admission.
Background usage
For asynchronous work originating in a gateway request, retain the verified context in the same running process and report the final batch after the response. This is still reporting for that served request, not an independent cron or arbitrary historical usage API:
async function runBatch(ctx: FartherShoreRequestContext, job: BatchJob) {
const elapsedSeconds = await executeBatch(job);
await ctx.report({
meter: "compute_seconds",
values: { seconds: elapsedSeconds },
});
}
The context includes runtime behavior and cannot be JSON-serialized into a
durable job queue and reconstructed as a trusted context. Post-stream delivery
requires the originating request's valid attestation and supports one callback.
If work can outlive that delivery window or process, design and verify its
settlement workflow before using this pattern. Call fs.shutdown() during
graceful termination; shutdown is not durable queue storage.
Contract pairing
Backend code cannot invent a billable dimension. Declare the measures and meter, bind the meter to the route, and price it in a catalog a plan binds:
import * as fs from "@farthershore/business";
const inputTokens = fs.measure("input_tokens");
const outputTokens = fs.measure("output_tokens");
const model = fs.dimension("model");
const modelUsage = fs.meter("model_usage", {
measures: [inputTokens, outputTokens],
dimensions: [model],
});
const modelPricing = fs.pricing("model_usage", {
meter: modelUsage,
catalog: [
fs.rate.perMillion(fs.money.usd(3)).for(inputTokens),
fs.rate.perMillion(fs.money.usd(15)).for(outputTokens),
],
});
const api = fs.backend("api", { default: true });
const complete = fs.route("/v1/complete", {
post: { backend: api },
});
fs.meterRoutes("complete-model-usage", complete, {
reports: [modelUsage],
maxOutputUnits: outputTokens.atMost(8192),
});
fs.plan("payg", {
kind: fs.plan.kind.usage,
usagePricing: modelPricing.current(),
grants: [complete],
maxMonthlySpendCents: 50_000,
});
export default fs.business();
Then build and push the contract before deploying code that reports the new
meter. A runtime token can narrow the allowlist further with --meters or
--routes; it cannot widen the compiled business contract.