marginfuse

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 14 Imported by: 0

README

marginfuse-go

Go Reference ci license

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 panic into your code, and it does not block your request on MarginFuse being up. If MarginFuse is unreachable, your requests proceed unchanged.
  • Zero dependencies. Standard library only, Go 1.21+.

Server side only. This SDK carries a secret API key. Never ship it in a binary a user can run and read.

Install

go get github.com/marginfuse/marginfuse-go

Track an AI call

Monitoring. One call after each AI request, metadata only.

mf, err := marginfuse.New(marginfuse.Config{APIKey: os.Getenv("MARGINFUSE_KEY")})
if err != nil {
    return err
}
defer mf.Close() // flushes

mf.Track(marginfuse.TrackParams{
    CustomerID: "cus_8x2m91", // your Stripe customer id, or your own
    Feature:    "ai_chat",
    Provider:   "openai",
    Model:      "gpt-4.1",
    Usage:      marginfuse.Usage{InputTokens: 1204, OutputTokens: 388},
})

Track returns immediately and sends in the background with retries. In a worker, a cron job or a Lambda handler, call Flush before the process exits, or the last events go with it.

A zero in Usage means not reported, not "used none": the field is left off the request entirely, because claiming a call used zero input tokens is a different statement from not knowing what it used.

Guard a call

Protection. Ask before the call runs, and act on the answer.

out, err := mf.Guard(ctx,
    marginfuse.DecideParams{
        CustomerID: "cus_8x2m91",
        Feature:    "ai_chat",
        Provider:   "openai",
        Model:      "gpt-4.1",
    },
    func(ctx context.Context, d marginfuse.Decision) (marginfuse.ProviderCall, error) {
        // d.Model is the one to call: a downgrade verdict changes it.
        r, err := client.CreateChatCompletion(ctx, request(d.Model, messages))
        if err != nil {
            return marginfuse.ProviderCall{}, err
        }
        return marginfuse.ProviderCall{
            Usage: marginfuse.Usage{
                InputTokens:  r.Usage.PromptTokens,
                OutputTokens: r.Usage.CompletionTokens,
            },
        }, nil
    })

if err != nil {
    return err // your provider's error, unchanged
}
switch out.Kind {
case marginfuse.GuardCompleted:
    // the call ran
case marginfuse.GuardTopupRequired:
    showTopup(out.Decision.TopupContext)
case marginfuse.GuardBlocked:
    showLimitReached()
}

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 Guard 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 function is never called.

Why Decide returns no error

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 Config.OnError.

Tell MarginFuse what a customer pays

Margin needs a revenue side. With Stripe connected it comes from there. Without one, you declare your plans in MarginFuse and say which plan each customer is on:

id, err := mf.Identify(ctx, marginfuse.IdentifyParams{
    CustomerID: "user_8x2m91",
    Plan:       "pro", // the key of a plan you declared in Settings
    Name:       "Acme Studio",
    Metadata:   map[string]string{"tier": "legacy"},
})
if err != nil {
    log.Printf("marginfuse identify: %v", err)
}

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 method that returns an error, and the only one that should. Decide fails open and Track retries, because both have a safe default; "I could not record what this customer pays" has none, and a wrong plan is a wrong margin.

DecideParams and TrackParams also carry 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.

var body struct {
    Usage marginfuse.OpenRouterUsage `json:"usage"`
}
json.Unmarshal(raw, &body)

usage, cost := marginfuse.FromOpenRouter(&body.Usage)

mf.Track(marginfuse.TrackParams{
    CustomerID: "cus_8x2m91",
    Feature:    "ai_chat",
    Provider:   "openrouter",
    Model:      "anthropic/claude-sonnet-4.5",
    Usage:      usage,
    CostUSD:    cost,
})

Use the helper 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 strconv.FormatFloat with 'g' produces "1.2e-07" for small costs and the API rejects that.

Configuration

mf, err := marginfuse.New(marginfuse.Config{
    APIKey:     os.Getenv("MARGINFUSE_KEY"),
    BaseURL:    "https://api.marginfuse.com", // your own deployment in dev
    Timeout:    1500 * time.Millisecond,      // Decide budget before failing open
    OnError:    func(err error, ctx string) { log.Printf("marginfuse %s: %v", ctx, err) },
    HTTPClient: myClient,
})

OnError is the only place transport failures surface. The SDK swallows them so they cannot become your outage; without the hook they are silent.

What it sends

Everything, and nothing else:

eventId  customerId  feature  provider  model  requestedModel
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.

git clone --recurse-submodules https://github.com/marginfuse/marginfuse-go
cd marginfuse-go
go test ./...                          # unit tests, plus the shared gateway vectors
npm --prefix contract/harness install
npm --prefix contract/harness run conformance go

MIT, Pemira Labs.

Documentation

Overview

Package marginfuse is the 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.

Reliability contract: this SDK never panics into application code and never blocks a request on MarginFuse availability. Decide fails open to ActionAllow on any timeout or error; Track and Acknowledge retry in the background and surface problems only through Config.OnError.

Server side only: it carries a secret API key.

Index

Constants

View Source
const ContractVersion = 2

ContractVersion is the version of the shared SDK contract this build was verified against.

Module versions differ per language, because each tracks its own breaking changes: a rename in Python must not tell Go users something broke. What makes the SDKs interchangeable is this, not the module version. Two SDKs reporting the same contract version have passed the same scenarios and the same vectors.

See github.com/marginfuse/sdk-contract.

View Source
const Version = "0.3.0"

Version is the released version of this module, as sent in the user-agent.

A Go module has no manifest to read this from: the version is the git tag, and the module proxy caches a tag permanently the first time anyone fetches it, so a wrong one cannot be corrected in place. The release workflow therefore refuses to publish a tag that disagrees with this constant.

The Node SDK shipped two releases reporting 0.1.0 because it had a literal like this one with nothing checking it.

Variables

This section is empty.

Functions

This section is empty.

Types

type Acknowledgment

type Acknowledgment string

Acknowledgment is what the application actually did with a decision.

const (
	AckProceededAsRequested      Acknowledgment = "proceeded_as_requested"
	AckUsedDowngradeModel        Acknowledgment = "used_downgrade_model"
	AckPresentedTopup            Acknowledgment = "presented_topup"
	AckBlockedBeforeProviderCall Acknowledgment = "blocked_before_provider_call"
	AckFailedToApply             Acknowledgment = "failed_to_apply"
)

type Action

type Action string

Action is a verdict. Enforce on this alone.

const (
	ActionAllow         Action = "allow"
	ActionDowngrade     Action = "downgrade"
	ActionTopupRequired Action = "topup_required"
	ActionBlock         Action = "block"
)

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is safe for concurrent use.

func New

func New(cfg Config) (*Client, error)

New returns a Client. It returns an error only for a missing API key, because that is a programming mistake rather than a runtime condition.

func (*Client) Acknowledge

func (c *Client) Acknowledge(decisionID string, ack Acknowledgment)

Acknowledge tells MarginFuse what your application did with a decision.

func (*Client) Close

func (c *Client) Close()

Close flushes and stops accepting background work.

func (*Client) Decide

func (c *Client) Decide(ctx context.Context, p DecideParams) Decision

Decide asks whether the next call should run. It always returns a verdict.

There is no error return on purpose. A failed decision is not a condition the caller should branch on: it is an allow with Degraded set, because MarginFuse being unreachable must never become your outage. Transport failures go to Config.OnError.

func (*Client) Flush

func (c *Client) Flush(ctx context.Context)

Flush waits for queued events and acknowledgments. It never panics, and it returns when ctx is done even if work is still in flight.

func (*Client) Guard

Guard runs the whole loop: ask, run, report, acknowledge.

run receives the decision and must return what the call consumed. Use decision.Model: a downgrade verdict changes it.

It takes a callback rather than returning a decision for you to act on, because enforcement must not depend on the caller remembering to check anything. When the verdict is block, run is never invoked.

An error from run is returned unchanged: your error handling owns provider failures. The attempt is recorded before it is returned, because the provider may still have charged for it.

func (*Client) Identify added in v0.2.0

func (c *Client) Identify(ctx context.Context, p IdentifyParams) (Identity, error)

Identify tells MarginFuse who a customer is and what plan they are on.

Plan is the key of a plan you declared in MarginFuse Settings, not a Stripe price id. MarginFuse derives that customer's revenue from the plan's price for every cycle, which is what makes margin per customer and margin policies work with no revenue source connected. Those figures are labeled as a declared price wherever they appear, because nobody confirmed collection.

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 at that moment and prorates what accrued.

This is the one call that returns an error, and the only one that should. Decide fails open and Track retries, because both have a safe default; "I could not record what this customer pays" has none, and a wrong plan is a wrong margin. The error also goes to Config.OnError.

func (*Client) Track

func (c *Client) Track(p TrackParams)

Track reports a call that already happened. It returns immediately and sends in the background with retries.

Call Flush before a process exits, or the last events go with it.

func (*Client) TrackAndWait

func (c *Client) TrackAndWait(ctx context.Context, p TrackParams)

TrackAndWait is Track for jobs and scripts that must not exit early.

type Config

type Config struct {
	// APIKey is your project API key. Required.
	APIKey string

	// BaseURL points at your own deployment in development.
	BaseURL string

	// Timeout is how long Decide waits before failing open. Default 1.5s.
	Timeout time.Duration

	// OnError receives transport failures the SDK swallowed. Without it they
	// are silent by design: this SDK is in your request path and must not
	// become your outage.
	OnError func(err error, context string)

	// HTTPClient replaces the default. Useful for proxies and test doubles.
	HTTPClient *http.Client
}

Config configures a Client. Every field except APIKey has a usable zero value.

type DecideParams

type DecideParams struct {
	CustomerID    string
	Provider      string
	Model         string
	Plan          string
	Feature       string
	ExpectedUsage Usage
}

DecideParams asks about the call you are about to make.

Plan is the key of a plan you declared in MarginFuse. It is a hint: a key that does not resolve is ignored rather than failing the decision.

type Decision

type Decision struct {
	ID             string `json:"id,omitempty"`
	Action         Action `json:"action"`
	Model          string `json:"model"`
	Provider       string `json:"provider"`
	TopupContext   string `json:"topupContext,omitempty"`
	Degraded       bool   `json:"degraded"`
	DegradedReason string `json:"degradedReason,omitempty"`
}

Decision is a verdict from MarginFuse.

Degraded is true when MarginFuse could not reach a verdict and the request was allowed through unprotected. ID is empty in that case, which is exactly why enforcement must depend on Action alone.

type GuardKind

type GuardKind string

GuardKind is what Guard did.

const (
	GuardCompleted     GuardKind = "completed"
	GuardBlocked       GuardKind = "blocked"
	GuardTopupRequired GuardKind = "topup_required"
)

type GuardOutcome

type GuardOutcome struct {
	Kind     GuardKind
	Decision Decision
}

GuardOutcome is the result of the whole loop.

type IdentifyParams added in v0.2.0

type IdentifyParams struct {
	CustomerID  string
	Plan        string
	ClearPlan   bool
	PeriodStart time.Time
	Name        string
	Email       string
	Metadata    map[string]string
}

IdentifyParams says who a customer is and what plan they pay for.

Plan is the key of a plan you declared in MarginFuse Settings, not a Stripe price id. Leave it empty to change nothing about the plan; set ClearPlan to take the customer off plans entirely. PeriodStart backdates the cycle for a customer who has been paying since an earlier date.

type Identity added in v0.2.0

type Identity struct {
	CustomerID  string `json:"customerId"`
	Plan        string `json:"plan"`
	PeriodStart string `json:"periodStart,omitempty"`
	PeriodEnd   string `json:"periodEnd,omitempty"`
}

Identity is what MarginFuse recorded for a customer.

Plan is empty when the customer is on none.

type OpenRouterPromptDetails

type OpenRouterPromptDetails struct {
	CachedTokens     float64 `json:"cached_tokens"`
	CacheWriteTokens float64 `json:"cache_write_tokens"`
	AudioTokens      float64 `json:"audio_tokens"`
}

OpenRouterPromptDetails is the cache breakdown inside prompt_tokens.

type OpenRouterUsage

type OpenRouterUsage struct {
	PromptTokens        float64                  `json:"prompt_tokens"`
	CompletionTokens    float64                  `json:"completion_tokens"`
	Cost                *float64                 `json:"cost"`
	PromptTokensDetails *OpenRouterPromptDetails `json:"prompt_tokens_details"`
}

OpenRouterUsage is the shape this helper reads from an OpenRouter usage object. Structural on purpose: it accepts a decoded response from any client without either side importing the other's types.

type Outcome

type Outcome string

Outcome is what happened to a provider call.

const (
	OutcomeSuccess       Outcome = "success"
	OutcomeProviderError Outcome = "provider_error"
	OutcomeAppCancelled  Outcome = "app_cancelled"
	OutcomeTimeout       Outcome = "timeout"
)

type ProviderCall

type ProviderCall struct {
	Usage   Usage
	CostUSD string
	Outcome Outcome
}

ProviderCall is what your callback did, handed back to Guard so it can be reported.

CostUSD is a decimal string, not a float: money that round-trips through a float stops being the number the provider charged.

type TrackParams

type TrackParams struct {
	EventID         string
	CustomerID      string
	Provider        string
	Model           string
	Plan            string
	Feature         string
	RequestedModel  string
	Usage           Usage
	CostUSD         string
	OccurredAt      time.Time
	Outcome         Outcome
	DecisionID      string
	RetryOfEventID  string
	CorrectsEventID string
}

TrackParams reports a call that already happened.

EventID is the idempotency key. Leave it empty and one is generated; set it yourself when you already have an id you can retry with safely.

type Usage

type Usage struct {
	InputTokens         int     `json:"inputTokens,omitempty"`
	OutputTokens        int     `json:"outputTokens,omitempty"`
	CachedInputTokens   int     `json:"cachedInputTokens,omitempty"`
	CacheCreationTokens int     `json:"cacheCreationTokens,omitempty"`
	Images              int     `json:"images,omitempty"`
	AudioSeconds        float64 `json:"audioSeconds,omitempty"`
}

Usage is what a provider call consumed.

Zero means not reported, not "used none": the field is omitted from the request entirely, because claiming a call used zero input tokens is a different statement from not knowing what it used. Report what you have.

func FromOpenRouter

func FromOpenRouter(u *OpenRouterUsage) (Usage, string)

FromOpenRouter maps an OpenRouter usage object to the MarginFuse fields.

var body struct{ Usage marginfuse.OpenRouterUsage `json:"usage"` }
json.Unmarshal(raw, &body)
usage, cost := marginfuse.FromOpenRouter(&body.Usage)
mf.Track(marginfuse.TrackParams{..., Usage: usage, CostUSD: cost})

It exists because mapping the fields by hand gets two things silently wrong, and both misstate margin without producing an error anywhere:

First, prompt_tokens is the TOTAL input count. Cached reads and cache writes are already inside it, and MarginFuse prices those as three separate charges and adds them up, so passing the total straight through charges every cached token twice at the full uncached rate.

Second, cost is a float, and strconv.FormatFloat with 'g' renders small ones in exponent notation ("1.2e-07"), which the API rejects as a decimal string.

The returned cost is empty when the response carried none, which lets the event fall through to MarginFuse's own pricing instead of claiming a $0 charge.

Directories

Path Synopsis
cmd
conformance-runner command
Command conformance-runner drives this SDK through one shared conformance scenario.
Command conformance-runner drives this SDK through one shared conformance scenario.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL