Enlace Metering MCP
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Enlace Metering MCPshow me how to set a monthly credit cap for a specific user principal"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
enlace_metering
Usage tracking and credit/quota access-gating for enlace MCP connectors — the platform's "usage & access-policy" layer, sibling to enlace_auth (identity) and enlace_connector (deploy).
Whether a principal may use a resource is a strategy (a pure function), and recording what they spent is a separate concern from deciding whether to let them. That separation is the core design principle (see Research below).
import enlace_metering as em
# 1. record (the meter) — a durable ledger over any MutableMapping (dict here; a
# dol file store in prod, growing to s3dol/sqldol untouched).
ledger = em.UsageLedger(store={})
# 2. gate (the strategy) — parameterized credit formulas, composable.
policy = em.per_principal(
{"owner@example.com": em.unlimited()}, # owner: no cap
default=em.require_all( # everyone else:
em.per_call_cap(2.00), # ≤ $2 per call, AND
em.credit_cap(20.00, period="month"), # ≤ $20 / calendar month
),
)
# 3. enforce (the glue) — one FastMCP middleware over EVERY tool call, fail-closed.
# (needs `pip install enlace_metering[mcp]`)
from enlace_metering import MeteringMiddleware
mw = MeteringMiddleware(ledger, policy=policy, allowed={"owner@example.com"})
# app = py2mcp.http.mk_http_app(refs, auth=AUTH, middleware=[mw], stateless_http=True)Why (the three concerns)
Concern | Where | Shape |
Enforce — may this call proceed? |
| pure |
Record — what did it cost? |
|
|
Glue — identity + gate + write-ahead |
| a FastMCP |
The gate runs before the ledger sees the event — enforcement is a distinct layer from billing. The core (policy + ledger) is dependency-free and unit-testable against a dict; only the middleware needs fastmcp.
Related MCP server: ai-credits-mcp
Policies (the strategy library)
All are constructors returning a GatePolicy; compose with require_all / per_principal.
unlimited()— always allow (record only).credit_cap(usd_per_period, period="month")— prepaid/metered cap per calendar period (resets on the boundary).rolling_window_cap(usd, window=timedelta(days=1))— sliding-window cap (rate control).per_call_cap(usd)— reject a single call whose estimated cost exceedsusd.call_count_cap(n, period="day")— count-based quota (no cost model needed).balance_gate(get_balance)— true prepaid credits: deny when a host-managed balance can't cover the call.per_principal({email: policy}, default=…)— tiered plans (owner unlimited, others capped).require_all(*policies)— AND / most-restrictive (first denial wins).
Forward-looking gating (deny a call that would cross a cap) needs a proposed-cost estimate — pass estimate_cost=(tool, args) -> usd|None to the middleware. Without one, caps block on already-accumulated usage ("you've hit your limit").
Ledger layout
Keyed {principal}/{YYYY-MM}/{id}.json so "what did X spend this month" is a cheap subtree scan. The middleware writes write-ahead (started → done/error under one id): a costed call never spends on a shared key without a recoverable trace. The store is injected, so it grows from a local dol.Files to s3dol/sqldol without touching callers.
Caller secrets in error text
A failed call's error is recorded in the ledger and re-raised to the client, and a provider can quote a key back in it (ElevenLabs does on a 401): the caller's bring-your-own key, or the connector's own key. So the middleware passes that text through a redactor first, and re-raises a scrubbed stand-in (same type where it can be rebuilt, else ToolError) with no chain back to the dirty original.
The default, redact_secrets, replaces in one pass, longest first:
the value of every in-flight request header named like a credential (
Authorization,X-<Provider>-Key,-Token,-Secret) with<redacted:{header}>;the value of every environment variable named like one (a
_-separatedKEY,API_KEY,TOKEN,SECRET,PASSWORDorPWsegment:ELEVENLABS_API_KEY,FAL_KEY,SHARED_PW_X) with<redacted:env:{NAME}>.
So a new BYO-key header or a new provider key in the environment is covered without configuration. For a secret named otherwise, MeteringMiddleware(ledger, redact=secret_redactor(env_names=["MY_URL_WITH_TOKEN"])); any redact(text) -> text works. If the redactor raises, the error text is withheld rather than recorded unredacted.
Each secret is also matched URL-encoded and per :-separated part (fal keys are id:secret), and any env var whose value is a URL with a password in it is redacted whatever its name. Names that describe a credential rather than hold one (S3_KEY_PREFIX, SSH_KEY_PATH, TOKEN_URL, AWS_ACCESS_KEY_ID) are left alone. A provider that quotes only a truncated prefix is out of reach.
fastmcp logs a failing tool's traceback before the middleware sees the exception, so the middleware also redacts log records, with the in-flight call's redactor (install_log_redaction(), a chained LogRecordFactory, so loggers created later are covered too). By default (redact_logs="*", i.e. ALL_LOGGERS) that is every record in the process: provider SDKs (httpx, fal_client, ElevenLabs' fern), the root logger, uvicorn.error, asyncio, plus what warnings.warn shows (warnings.showwarning is wrapped, so a warning still goes where it went before), the traceback an uncaught exception prints in any thread, and what an exception ignored in __del__ prints (sys.unraisablehook). A dirty uncaught exception still reaches the previous hook (a crash reporter, say), as a stand-in that prints redacted. A dirty log argument is redacted in place, so formatters that unpack record.args (uvicorn's access log) keep working. To cover records logged at import time too, call install_log_redaction(namespaces="*") as the first statement of server.py. redact_logs=("mypkg",) limits it to fastmcp plus those namespaces, redact_logs=True to fastmcp alone, and redact_logs=False opts out. The install also wraps logging.Logger.handle, so every record routed through any logger is redacted, extra= fields included, whichever handler emits it: handlers configured later (uvicorn's dictConfig at mcp.run()), logging.lastResort, and records that never went through the factory (logging.makeLogRecord, a logger class with its own makeRecord). RedactingLogFilter does the same job for any logger or handler you choose. A caller's bring-your-own header key is known process-wide while their call is in flight and for a while after (the last RECENT_CALLS_KEPT = 64 calls, at most RECENT_CALLS_SECONDS = 300 s), so a record logged or an exception raised from a raw threading.Thread (which does not inherit the request's context), even one that outlives the call, is redacted of it too; past those bounds it is not. Out of reach, like a truncated prefix: a key that a formatter shortened before it was logged, such as asyncio's default exception handler, whose Handle repr reprlib-truncates a callback's arguments to head and tail ('sk-abcde...vwxyz'), and text a program writes to stderr itself (print(..., file=sys.stderr)).
Install
pip install enlace_metering # core: policy + ledger (no deps)
pip install 'enlace_metering[mcp]' # + the FastMCP middlewareResearch — how usage-based metering / quotas are built
The design follows the settled industry pattern: entitlement/quota enforcement is a separate layer from the billing meter — enforcement decides before usage is recorded [1][2]. Common formulas this package parameterizes:
Prepaid credits / credit blocks — buy a bundle, burn down; blocks carry their own amount + expiry with priority burn-down [3][4]. →
credit_cap,balance_gate.Metered caps + real-time balance / grace — per-feature quotas with live balance tracking [1][5]. →
credit_cap,call_count_cap.Token-bucket rate limiting — tokens replenish at a rate; a request costs N; deny if insufficient [2]. →
rolling_window_cap(the sliding-window cousin).
Keeping the decision a pure strategy (not baked into the middleware) is what lets a host swap "owner unlimited" for "prepaid credits" or "monthly cap" without touching the connector — the open-closed shape the enlace family favors.
References
Lago. How to architect billing systems to power usage-based pricing. getlago.com/blog/architect-billing-systems
OpenMeter. Metering and billing for AI, API and DevOps. github.com/openmeterio/openmeter
Kong. Stop AI token overspend: prepaid credits in Konnect. konghq.com/blog/product-releases/metering-billing-prepaid-credits
Solvimon. AI billing software built for tokens, credits, and inference pricing. solvimon.com/blog/6-ai-billing-software-platforms-built-for-credits
Zuora. Metered billing: architecture, metrics, and monetization models. zuora.com/glossary/metered-billing
This server cannot be deployed
Maintenance
Related MCP Connectors
Usage metering + SLA accounting for agent services (the meter behind x402 micropayments).
Metered MCP tools: free discovery over MCP; per-call execution settled in USDC via x402 v2.
A paid remote MCP for hosted MCP server, built to return verdicts, receipts, usage logs, and audit-r
Meter, cap, and block AI agent spend before the provider is charged.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenancePer-call billing and metering proxy for MCP tool servers. Providers set pricing via the open MCP Billing Spec (MIT), consumers pay through Stripe Connect with signed receipts and SLA monitoring.MIT
- FlicenseNot gradedqualityDmaintenanceMCP server that exposes AI credit usage data from OpenMeter to the TAIS AI chat service, automatically scoped to the calling tenant and restricted to admin users.-
- AlicenseNot gradedqualityCmaintenanceReference implementation for adding subscription-based gating to MCP endpoints, covering auth, plan entitlements, usage metering, and multi-tenant scoping.MIT
- FlicenseNot gradedqualityBmaintenanceEnforces default-deny policies, budgets, and tamper-evident audit logging for MCP tool calls before they reach upstream servers.-