MarginFuse 0.3.1
dotnet add package MarginFuse --version 0.3.1
NuGet\Install-Package MarginFuse -Version 0.3.1
<PackageReference Include="MarginFuse" Version="0.3.1" />
<PackageVersion Include="MarginFuse" Version="0.3.1" />
<PackageReference Include="MarginFuse" />
paket add MarginFuse --version 0.3.1
#r "nuget: MarginFuse, 0.3.1"
#:package MarginFuse@0.3.1
#addin nuget:?package=MarginFuse&version=0.3.1
#tool nuget:?package=MarginFuse&version=0.3.1
MarginFuse for .NET
Server-side SDK for MarginFuse: profitability guardrails for AI SaaS. Connect revenue to per-request AI cost, see gross margin per customer, and stop loss-making requests before they run.
- Metadata only, by construction. The event shape has no field for prompts or responses, so they cannot be sent. Not a policy, an absence.
- Never breaks your app. It does not throw into your code, and it does not block your request on MarginFuse being up. If MarginFuse is unreachable, your requests proceed unchanged.
- Zero dependencies.
net8.0, usingSystem.Text.Jsonfrom the box. Nothing to conflict with what your application already references.
Server side only. This SDK carries a secret API key. Never ship it in a desktop or mobile application, or anything else a user can read.
Install
dotnet add package MarginFuse
Track an AI call
Monitoring. One call after each AI request, metadata only.
await using var mf = new MarginFuseClient(new MarginFuseOptions
{
ApiKey = Environment.GetEnvironmentVariable("MARGINFUSE_KEY")!,
});
mf.Track(new TrackParams
{
CustomerId = "cus_8x2m91", // your Stripe customer id, or your own
Feature = "ai_chat",
Provider = "openai",
Model = "gpt-4.1",
Usage = new Usage { InputTokens = 1204, OutputTokens = 388 },
});
Track returns immediately and sends in the background with retries. In a
worker or a short-lived process, flush before exiting. The client is
IAsyncDisposable, so await using does it for you.
A null property in Usage means not reported, not "used none": it is left off
the request entirely, because claiming a call used zero input tokens is a
different statement from not knowing what it used.
With dependency injection
The client is safe to share, so register it once:
builder.Services.AddSingleton(_ => new MarginFuseClient(new MarginFuseOptions
{
ApiKey = builder.Configuration["MarginFuse:ApiKey"]!,
OnError = (error, context) => logger.LogWarning(error, "marginfuse {Context}", context),
}));
Guard a call
Protection. Ask before the call runs, and act on the answer.
var outcome = await mf.GuardAsync(
new DecideParams
{
CustomerId = "cus_8x2m91",
Feature = "ai_chat",
Provider = "openai",
Model = "gpt-4.1",
},
async decision =>
{
// decision.Model is the one to call: a downgrade verdict changes it.
var response = await client.ChatAsync(decision.Model, messages);
var cached = response.CachedTokens; // default to 0 when absent
return new ProviderCall<ChatResponse>
{
Result = response,
Usage = new Usage
{
InputTokens = response.PromptTokens - cached,
CachedInputTokens = cached,
OutputTokens = response.CompletionTokens,
},
};
});
switch (outcome.Kind)
{
case GuardKind.Completed: Use(outcome.Result!); break;
case GuardKind.TopupRequired: ShowTopup(outcome.Decision.TopupContext); break;
case GuardKind.Blocked: ShowLimitReached(); break;
}
The provider client in this example is illustrative. For OpenAI usage, report
inputTokens = prompt_tokens - cached_tokens and cachedInputTokens = cached_tokens
(default cached tokens to zero when absent); map these fields from your provider
client's response. The example assumes a same-provider downgrade. If policies
can switch providers, dispatch using the decision's provider as well as its model.
One call does the whole loop: ask, run with the resolved model, report the real cost, acknowledge what your application did.
Why a callback
Enforcement must not depend on you remembering to check anything. If
GuardAsync returned a decision for you to act on, forgetting the check once
would mean a blocked request reaches the provider anyway. With a callback that
is structurally impossible: when the verdict is Block, your lambda is never
invoked.
Why DecideAsync has no failure path
There is no failure a caller should branch on. A decision that times out or
errors is an allow with Degraded set, because MarginFuse being unreachable
must never become your outage. Transport failures go to OnError.
Tell MarginFuse what a customer pays
Margin needs a revenue side: Stripe for web billing, RevenueCat for App Store and Google Play proceeds, or declared plan prices. RevenueCat joins by App User ID; use that same ID in your events. Without a billing connection, declare your plans in MarginFuse and say which plan each customer is on. Declared revenue is unverified and does not confirm payment:
Identity id = await mf.IdentifyAsync(new IdentifyParams
{
CustomerId = "user_8x2m91",
Plan = "pro", // the key of a plan you declared in Settings
Name = "Acme Studio",
Metadata = new Dictionary<string, string> { ["tier"] = "legacy" },
});
if (!id.Ok) logger.LogWarning("MarginFuse identify: {Error}", id.Error);
Safe to call on every sign-in: sending the plan the customer is already on
changes nothing. Sending a different one ends the current cycle and prorates
what accrued. PeriodStart backdates the cycle for a customer who has been
paying since an earlier date; ClearPlan takes them off plans.
This is the one call that does not fail open. Track retries later and
DecideAsync allows, because both have a safe default; "I could not record
what this customer pays" has none, and a wrong plan is a wrong margin. So it
reports the failure to you instead of swallowing it. It still never throws.
TrackParams and DecideParams also take a Plan, so it can ride along with
usage rather than needing its own call. There it is a hint: a key that does not
resolve is ignored rather than failing your event.
OpenRouter and other gateways
Gateways report the real cost of every call. Forward it and your figures are exact instead of estimated.
// usage is the decoded "usage" object from the response
var mapped = OpenRouter.From(usage);
mf.Track(new TrackParams
{
CustomerId = "cus_8x2m91",
Feature = "ai_chat",
Provider = "openrouter",
Model = "anthropic/claude-sonnet-4.5",
Usage = mapped.Usage,
CostUsd = mapped.CostUsd,
});
OpenRouter.From takes a JsonElement, so no particular HTTP client is
implied. Use it rather than mapping the fields yourself: OpenRouter's
prompt_tokens already includes cached reads and cache writes, which MarginFuse
prices separately, so passing it through directly charges every cached token
twice at the full input rate. The helper also formats the cost as a decimal
string, because the default numeric formatting produces 1.2E-07 for small
costs and the API rejects that.
Configuration
new MarginFuseOptions
{
ApiKey = Environment.GetEnvironmentVariable("MARGINFUSE_KEY")!,
BaseUrl = "https://api.marginfuse.com", // your own deployment in dev
Timeout = TimeSpan.FromMilliseconds(1500), // decide budget before failing open
OnError = (error, context) => log.Warn(error, context),
HttpClient = myClient, // proxies, IHttpClientFactory
}
OnError is the only place transport failures surface. The SDK swallows them so
they cannot become your outage; without the handler they are silent.
What it sends
Everything, and nothing else:
eventId customerId feature provider model requestedModel plan
usage { inputTokens, outputTokens, cachedInputTokens,
cacheCreationTokens, images, audioSeconds }
costUsd occurredAt outcome decisionId retryOfEventId correctsEventId
There is no field for message content anywhere in the wire types. The conformance suite checks this against the bytes that actually leave the process, on every scenario.
Conformance
This SDK is verified against
marginfuse/sdk-contract, the same
contract every MarginFuse SDK in every language is held to. It is a submodule
here, so the pinned commit records exactly which contract a release passed, and
Contract.Version reports it at runtime.
git clone --recurse-submodules https://github.com/marginfuse/marginfuse-dotnet
cd marginfuse-dotnet
dotnet test # unit tests, plus the shared gateway vectors
dotnet build tools/ConformanceRunner/ConformanceRunner.csproj -c Release
npm --prefix contract/harness install
npm --prefix contract/harness run conformance dotnet
Links
- MarginFuse, product and pricing
- Documentation
- API reference
- Security policy
- Contributing
MIT, Pemira Labs.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.