Farther ShoreDocs
Go to Farther Shore
Bring your own backend
Scaffold a backend service
Transport modes
Infrastructure with OpenTofu
Deploy on Railway
Deploy on Render
Deploy on AWS
Deploy on Google Cloud
Metering & verification
Verify before parsing or handlingChoose the correct metering channelContract pairing
Runtime tokens
Storing per-user data
@farthershore/backend
Add a backend
Connect a direct backend
Add a webhook consumer
@farthershore/backend exports
@farthershore/backend/express exports
@farthershore/backend/reflect exports
@farthershore/backend/runtime exports
@farthershore/backend/testing exports
@farthershore/backend/webhooks exports
backend-sdk HTTP contracts
Status
Docs/Connect your application/Metering & verification

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:

bash
pnpm add @farthershore/backend
ts
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:

ts
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:

ts
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:

MomentTransportNetwork call from handlerEffect
Structural request countfs.requests() in the business contractNoneCounted by the gateway; never reported by the backend.
Before the response is sentsigned in-band x-fs-metering headersNoneSettles the request's monetary reservation on the way out.
After the response is on the wire (streams)attested post-stream channelOne callbackRated under the request's served identity; billing-only.
Deferred work for the original served requestpost-stream channel with its retained verified contextOne callback total per served requestRated 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.

ts
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
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:

reasonWhat it means
token_not_publishedThe 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_revokedThe token exists at the edge but is revoked.
token_kind_mismatchThe token's fsrt_live_ / fsrt_test_ prefix disagrees with the kind it was minted as.
token_business_mismatchThe token belongs to a different business than the request's credential.
token_missing_metering_operationThe token was minted without the metering operation.
token_environment_mismatchThe token is scoped to one environment and the request authenticated into another.
token_backend_mismatchThe 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_mismatchThe published record does not match the presented token. Report this.
signature_mismatchThe signature did not verify against the presented token.
request_context_mismatchThe 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_headersSome but not all of the three metering headers arrived — usually a proxy stripping headers.
invalid_measurementsThe measurement lane was present but malformed.
parse_or_verify_errorThe 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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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.

PreviousDeploy on Google CloudNextRuntime tokens

On this page

Verify before parsing or handlingChoose the correct metering channelReport from the verified contextWhen the gateway cannot trust a reportPost-stream usageBackground usageContract pairing