Skip to content
inflowpayaiPublic

About

Go SDK for InFlow MPP and x402 Buyer and Seller integrations.

Topics

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

InFlow Go SDK

Go SDK for accepting and making InFlow payments through MPP and x402.

See the shared SDK compatibility and support policy for supported releases, dependency expectations, and security reporting.

For development checks and reproducible SDK conformance reports, see Shared conformance.

Run an example

The runnable examples walk through MPP and x402 payments with separate buyer and seller programs. They use Sandbox accounts, local HTTP sellers, and the public clients. Start there for setup commands, required credentials, expected output, subscriptions, and manual approval waiting or cancellation.

Using InFlow with the upstream Go SDKs

InFlow Go provides clients for making and accepting payments through InFlow. It uses the MPP Go SDK and the x402 Go SDK, but it is not a drop-in replacement for either library. Adopt the InFlow clients explicitly; changing an import does not preserve every upstream behavior. You can keep using upstream packages alongside InFlow.

In this guide, managed payments are handled through your InFlow account. External-wallet payments are signed by a wallet configured in your application. A payment credential or payment payload is the proof your application sends to the seller; obtaining it does not by itself confirm that the seller accepted the payment. A hook is an application callback: an after-hook runs after a payment payload has been created.

Adopting the MPP clients

The mpp/buyer client asks InFlow to obtain a payment credential rather than signing through a local wallet. Configure your InFlow account credentials and environment, then use Fulfil to obtain a credential or Do to pay an HTTP resource. The mpp/seller client uses an InFlow Seller account to validate and process payments. See Managed buyer credentials or Accepting payments for setup and examples.

The following are intentional InFlow behaviors. The comparison applies to mpp-go v0.2.0; upstream compatibility notes link to the relevant source.

Situation Upstream MPP Go InFlow Go What your application needs to do
A paid request already uses Authorization The buyer transport replaces it with the payment credential. Do rejects the conflict before obtaining a credential. Use a separate API-key header or cookie supported by the service. If the service only accepts application authentication in Authorization, its authentication design must accommodate MPP before this client can pay it. Manually sending the request does not resolve that conflict.
A request body cannot be replayed The transport creates a credential before trying to recreate the body. Do checks replayability before payment. Supply a working Request.GetBody for a request with a body.
A seller checks a payment The seller intent interface provides one Verify operation. Validate checks without consuming payment; Broadcast submits the credential for payment processing. Verify combines both. Use Validate only for a check. Use Verify or Protect when payment must be processed before serving the resource.
Your service offers subscriptions The charge convenience helpers select the charge intent. InFlow supports purchasing a subscription and authorizing access under an existing subscription. Sellers use seller.Offer.Subscription. Buyers pass a subscription challenge to Fulfil; supply PaymentOptions.SubscriptionID only to use an existing subscription rather than purchase one. See Accepting payments and Managed buyer credentials.
Your application forwards challenges or receipts Upstream types decode opaque challenge data and do not retain all InFlow top-level receipt fields. InFlow types preserve the encoded challenge fields and top-level receipt extensions. Use the InFlow MPP codecs throughout that exchange; decoding and rebuilding through upstream types can lose information.

Upstream limitation: MPP over MCP is not supported by the released mpp-go v0.2.0 integration or by InFlow Go. Do not assume the MPP-over-MCP integration available in InFlow Node is available here. InFlow Go does not define a competing transport while upstream support is absent. See the compatibility notes for the package reference.

Composing an x402 client

The x402 package aliases the upstream V2 payment types, so they can cross the package boundary without conversion. The x402/buyer client adds InFlow-managed payment creation and approval waiting. It can also use an upstream client supplied through Options.External for external-wallet payments. That client's registered schemes and spending controls remain active.

Situation InFlow behavior What your application needs to do
Both InFlow and an external wallet can pay Matching managed requirements take precedence; external signing is the fallback when none match. Configure Prefer for the managed scheme order. Use the upstream client directly for an external-wallet-only flow.
An external wallet is supplied to the combined client The client still loads InFlow account capabilities. Supply InFlow authentication; an external wallet does not make this combined client anonymous.
An application after-hook returns an error Wrapper hooks propagate it on both payment routes. Upstream x402 Go v2.27.0 discards errors from its own after-hooks. Register application callbacks through Options.Hooks when their errors must reach the caller. Hooks registered on External retain upstream behavior.
The paid HTTP response is another 402 Do returns it without creating a second payment. It does not run upstream callbacks that update payment state from the seller's response. Inspect the response before trying another payment. For an external-wallet scheme that requires those callbacks, use the upstream HTTP client instead of InFlow's Do.

These are wrapper behaviors, not changes to the x402 wire protocol. The upstream after-hook behavior is visible in its v2.27.0 client implementation. Tracked in upstream issue #3650. See x402 Buyer for setup and opt-in external-wallet sponsorship.

Waiting, retrying, and cancelling approvals

An InFlow payment can require a person's approval. Prepare creates the payment and returns a Payment object; Wait waits for its credential or payload. The two clients intentionally differ in what a failed wait means. This is an SDK lifecycle choice, not a requirement of MPP or x402.

Operation After a failed wait What your application needs to do
MPP Payment.Wait The payment object stores the error and attempts approval cancellation for up to five seconds. Later waits return that error. Cancelling any waiter abandons the payment for all waiters. Do not call Wait expecting it to resume. Inspect the outcome before deciding to start another payment.
x402 Payment.Wait The approval is not automatically cancelled. A later wait can poll the same transaction. Keep the handle and call Wait with a fresh context to resume, or call Cancel to abandon it. Resuming does not guarantee that the server will approve the payment.
x402 Client.Sign This one-shot operation attempts approval cancellation for up to five seconds after a failed wait. Use Prepare and Wait instead when your application needs to resume waiting.

For MPP, cancelling the context passed to Prepare cancels the payment operation, including subsequent waiting. Its WaitTimeout starts when creation returns, even if Wait has not been called yet. For x402, the preparation context applies only until Prepare returns; WaitTimeout starts separately for each new polling attempt. Concurrent x402 waits share the first caller's polling context. Cancelling a later caller stops only that caller's wait; cancelling the first stops the shared attempt, but a later attempt can resume it.

Once an x402 payload is received, the handle retains it and the result of its after-hooks. An after-hook failure is reported without cancelling the ready payment or rerunning the hook on another wait. Receiving a payload does not itself prove the seller accepted or settled it. Neither protocol's cancellation operation reverses a completed payment, and best-effort approval cleanup can fail. Inspect uncertain outcomes before starting another purchase.

Checking payment status

Both MPP and x402 Buyer clients expose PaymentStatus for a transaction you already created. Use its original identifier after an uncertain result or when the buyer needs to authenticate a card. This read is separate from waiting for an MPP credential or an x402 payload: receiving either does not prove the payment settled.

status, err := client.PaymentStatus(ctx, transactionID, inflow.PaymentStatusOptions{})
if err != nil {
    return err
}
if status.NextAction != nil && status.NextAction.Type == "authenticate_card" {
    // Present this dashboard URL to the buyer without attaching API credentials.
    fmt.Println("Authenticate your card:", status.NextAction.URL)
}
fmt.Println("Payment status:", status.Status)

client can be either Buyer client; transactionID is retained from the original payment. Each call fetches a fresh snapshot. The SDK returns unfamiliar status and action names unchanged, does not open action URLs, and does not create, confirm or cancel a payment. Canceling the context only stops the read. After the buyer finishes authentication, explicitly read the same transaction again. The absence of NextAction does not establish settlement.

There is one HTTP attempt by default. PaymentStatusOptions{Retries: 1} allows one retry of the read; retries are capped at three. Redirects are errors rather than instructions to forward your credentials. A failed read, including a 404, does not establish whether a prior payment completed and is not permission to create a replacement purchase. A returned GENERAL_ERROR is a successful status read describing a failed transaction, not an HTTP request failure.

Package design

One Go module carries one release version. MPP and x402 each have Core, Buyer, and Seller packages, with shared internal HTTP implementation. Payment clients accept an optional http.RoundTripper; InFlow controls their HTTP redirect and timeout policies. Construction performs no network activity. Operations load configuration when needed and permit a later attempt after a failed load.

TAP Seller verification

Import github.com/inflowpayai/inflow-go/tap/seller to verify the InFlow profile of Visa Trusted Agent Protocol. This optional package uses the Go standard library; it does not import payment clients or require InFlow account credentials. Verification recognizes the signing agent. It does not identify the buyer, grant account access, authorize a purchase, or prove that a payment settled.

verifier := seller.New(seller.Options{})
facts, err := verifier.Verify(ctx, seller.Request{
    Method: request.Method,
    URL: publicRequestURL,
    Headers: request.Header,
    Body: exactBodyBytes,
})

publicRequestURL is the absolute URL observed by the signer. Build it from trusted deployment configuration, not arbitrary forwarding headers. Preserve encoded paths, query order, and exact body bytes. Nil Body means absent; a non-nil empty slice means a supplied empty body and requires signed Content-Digest and Content-Type. The verifier does not consume an HTTP body stream or modify the supplied request.

WithVerified(ctx, request, func(Facts) error) calls the application only after signature verification and the atomic replay claim succeed. It returns verification, custom resolver/store, or callback errors to the caller. The application decides HTTP status and response formatting. See the runnable HTTP example for body limits, public-origin configuration, successful recognition, and rejection.

The profile accepts one sig2 Ed25519 signature covering method, authority, path, and query, plus digest and content type when a body is supplied. It accepts both ed25519 and Ed25519 spellings. Repeated signature parameters use their last value, in their first position, for both validation and canonical signature reconstruction. This is not permission to collapse ambiguous repeated HTTP fields or covered components.

Validity is checked when verification starts, with a maximum eight-minute signed interval. Key retrieval completing after expiration does not cause a second time check. Cryptographic success precedes replay storage. The default MemoryReplayStore retains key/nonce claims until signature expiry in one process. Reuse the verifier, and supply a shared atomic ReplayStore for multiple processes. This is not payment idempotency or permanent nonce storage.

NewVisaKeyResolver uses Visa's trusted key service, a one-hour fresh cache, a 24-hour outage-fallback window, and a three-second retrieval deadline. Configure KeyResolverOptions to supply a trusted URL, transport, clock, or durations; zero durations select defaults. A request's key identifier never chooses the URL. Successful refresh replaces the key set atomically. Unknown keys fail; a failed refresh can use a matching previously trusted key within the outage window. Key-set responses are limited to 8 MiB. Returned public-key bytes are independent copies, so modifying them does not change cached trust.

Concurrent refreshes share one request. As with the Go payment clients' shared configuration loads, the caller starting it owns its context; cancellation can fail that refresh for other callers. A waiting caller can cancel without cancelling the refresh. A later call can retry. Node does not expose this Go context boundary. Custom resolvers and stores must honor cancellation and support concurrent use.

Inspect *seller.Error.Code for SIGNATURE_INPUT_INVALID, CONTENT_DIGEST_INVALID, SIGNATURE_LIFETIME_INVALID, SIGNATURE_NOT_YET_VALID, SIGNATURE_EXPIRED, KEY_NOT_FOUND, KEY_RETRIEVAL_FAILED, SIGNATURE_INVALID, or NONCE_REPLAYED. Context cancellation and custom implementation errors retain their original identity. Returned facts include the key identifier, normalized algorithm, intent, nonce, timestamps, and signed component order. Keep any required investigation records under your application's data-retention policy.

x402 Core

Import github.com/inflowpayai/inflow-go/x402 for V2 payment types, InFlow configuration types, payment identifiers, and sponsorship declarations. Standard payment types are aliases of github.com/x402-foundation/x402/go/v2 v2.27.0 types: pass them to upstream APIs without conversions. Importing this package does not import blockchain signers or framework adapters.

id, err := x402.GeneratePaymentID(x402.DefaultPaymentIDPrefix)
if err != nil {
    return err
}
entry := x402.PaymentIdentifierEntry(x402.DeclarePaymentIdentifier(), id)
payload := x402.PaymentPayload{
    X402Version: x402.Version,
    Extensions: map[string]any{x402.PaymentIdentifier: entry},
}

This illustrates the extension field, not a complete signed payment. The declaration advertises required: false. ReadPaymentIdentifier and PaymentIdentifierEntry return nil for malformed declarations or invalid identifiers. Entries preserve additional information and schema fields, without modifying the supplied declaration. Use an empty prefix to generate an unprefixed identifier.

NormalizeDecimalString removes insignificant zeroes using string operations; it does not round amounts or convert them into atomic units. Non-plain notation such as 1e3 is returned unchanged. The upstream payload and extension maps can contain json.Number; when decoding JSON with large numeric proof values, use json.Decoder.UseNumber to avoid conversion to floating-point numbers. These types and declarations do not validate signatures, authorize payments, or execute sponsorship.

x402 Buyer

Import github.com/inflowpayai/inflow-go/x402/buyer to obtain managed payments or compose an external-wallet client from github.com/x402-foundation/x402/go/v2. New performs no requests. Supported loads the account's supported scheme/network pairs and caches them for one hour. Concurrent loads share one request; a failed load can be retried.

The combined Buyer client needs an InFlow API key or OAuth access token for that capability lookup: use Sandbox for testing or Production for live payments. Supplying an external wallet does not bypass this lookup. For external-wallet-only payments without an InFlow account, use the upstream client directly; the opt-in sponsorship extension below can also be registered on that client.

client, err := buyer.New(buyer.Options{
    Options: inflow.Options{
        Environment: inflow.Sandbox,
        APIKey: os.Getenv("INFLOW_API_KEY"),
    },
})
if err != nil {
    return err
}
payment, err := client.Sign(ctx, required, buyer.SignOptions{})

required is the seller's decoded V2 PaymentRequired. Sign prefers managed balance, then managed exact. Set Prefer to change that order. Policies filter candidates before selection. When several balance assets match, a fresh account balance lookup can select an affordable asset; an unavailable balance lookup falls back to the first match. InFlow remains the authority on available funds. Managed signing does not support Permit2.

For instrument payments, set buyer.Options.InstrumentID to an owned card's identifier. Leave it empty to use the account's primary card. A rejected selection fails without trying a different card. This option has no effect on balance or blockchain payments. The selection belongs to the client, as in the Node SDK; create separate clients when callers need different cards. For automatic selection through Sign or Do, include "instrument" in Prefer. Setting InstrumentID alone does not enable that scheme.

Set External to a configured upstream client for requirements that do not match managed capabilities. Its registered schemes and spending limits remain in effect. A policy rejection does not bypass the policy by switching to the external path. SignOptions.PaymentID and TransactionRequestExtensions apply to managed signing. The returned EncodedPayload preserves the server's signed encoding; use it directly as PAYMENT-SIGNATURE.

Waiting for approval

Use Select to choose a managed requirement, then pass a PaymentRequired containing that one requirement to Prepare. The returned Payment exposes ApprovalID, TransactionID, Status, Wait, and Cancel. Preparation creates the transaction once and does not poll.

selected, err := client.Select(ctx, required)
if err != nil {
    return err
}
if selected == nil {
    return errors.New("no InFlow-managed payment option matches this request")
}
selectedRequired := required
selectedRequired.Accepts = []x402.PaymentRequirements{*selected}
pending, err := client.Prepare(ctx, selectedRequired, buyer.SignOptions{})
if err != nil {
    return err
}
waitContext, stopWaiting := context.WithTimeout(ctx, 30*time.Second)
payment, err := pending.Wait(waitContext)
stopWaiting()

This example uses errors, time, and the x402 package alongside the Buyer client. required is the seller's decoded PaymentRequired. A nil selection means no managed option matches; Prepare is for managed payments only. Use Sign with a configured External client when you want the external-wallet fallback.

If this wait times out, keep pending and call pending.Wait with a fresh context to resume the same approval. To abandon it, call pending.Cancel with a live context. A timeout does not imply that the payment failed or that its approval was cancelled. Each wait attempt has its own WaitTimeout budget, defaulting to fifteen minutes; polling defaults to five seconds.

See Waiting, retrying, and cancelling approvals for concurrent waits, after-hook failures, and the difference from MPP's payment lifetime.

Application hooks and HTTP requests

Configure application callbacks through Options.Hooks. Before-hook errors stop payment creation. After-hook errors propagate on both managed and external-wallet paths. A failure hook can supply a replacement payload for one-shot signing, but not for a prepared payment tied to existing approval and transaction identifiers. Hooks registered directly on the upstream client retain upstream behavior, including its non-fatal after-hook errors. Callbacks must honor their contexts and support concurrent operations. Hook inputs are independent copies of payment data.

Do(request, options) sends an unpaid request and at most one paid replay. It preserves application authentication, does not attach InFlow API credentials to the resource request, and never follows redirects. A nonempty body must have Request.GetBody; replayability is checked before signing. The caller closes the returned response body. Do returns a second 402 to the caller rather than automatically authorizing another payment.

Some external-wallet payment schemes keep state that must be updated after reading the seller's response. InFlow's Do does not call those upstream response callbacks. For those schemes, use Newx402HTTPClient(external) and WrapHTTPClientWithPayment from github.com/x402-foundation/x402/go/v2/http instead. That upstream transport calls the scheme's response handlers and applies its own retry and redirect behavior; InFlow's single-paid-request and no-redirect guarantees do not apply to it.

External-wallet sponsorship

EIP-2612 support belongs to the upstream EVM exact signer. Configure its read-contract and typed-data signing capabilities; the upstream implementation can attach a permit when the seller advertises EIP-2612 sponsorship and Permit2 allowance is insufficient.

For InFlow's EIP-7702 sponsorship, import the opt-in x402/buyer/eip7702 package and register its extension on the upstream client. The main Buyer package does not import its blockchain dependencies.

extension, err := eip7702.New(eip7702.Options{
    Environment: inflow.Sandbox,
    Signer: wallet,
    Consent: confirmDelegation,
})
if err != nil {
    return err
}
external.RegisterExtension(extension)

wallet implements eip7702.Signer; confirmDelegation receives the context and delegation authorization and returns consent or an error. Delegation can persist even if the purchase fails, so obtain the owner's consent explicitly. The extension only handles advertised exact Permit2 payments. It checks allowance, uses the caller-configured InFlow preparation endpoint, verifies the exact approval/settlement batch and pinned contracts, and signs only after validating the operation hash. It never broadcasts. The signer must support concurrent calls and must honor cancellation. SignMessage applies Ethereum's personal-message prefix to the operation hash; it must not sign the hash as a raw transaction digest.

x402 Seller

Use x402/seller with the upstream x402 net/http middleware. InFlow supplies Seller configuration, priced payment offers, scheme registrations and a facilitator client. The upstream middleware checks payment, runs your handler and settles a successful response. You do not need to implement or host a facilitator.

Create an InFlow Seller account in Sandbox or Production, then create an API key in that dashboard. Set the matching Environment. A Developer account cannot load Seller configuration.

import (
    "context"
    "errors"
    "net/http"
    "os"

    inflow "github.com/inflowpayai/inflow-go"
    "github.com/inflowpayai/inflow-go/x402/seller"
    foundation "github.com/x402-foundation/x402/go/v2"
    xhttp "github.com/x402-foundation/x402/go/v2/http"
    "github.com/x402-foundation/x402/go/v2/http/nethttp"
)

func paidHandler(ctx context.Context, handler http.Handler) (http.Handler, error) {
    options := inflow.Options{Environment: inflow.Sandbox, APIKey: os.Getenv("INFLOW_API_KEY")}
    client, err := seller.New(options)
    if err != nil { return nil, err }
    facilitator, err := seller.NewFacilitator(options)
    if err != nil { return nil, err }
    route, err := client.Route(ctx, seller.RouteOptions{
        AcceptsOptions: seller.AcceptsOptions{Price: seller.PriceSpec{Amount: "$0.01"}},
    })
    if err != nil { return nil, err }
    if len(route.Accepts) == 0 { return nil, errors.New("no payment offers match this route") }
    registrations, err := client.SchemeRegistrations(ctx, seller.RegistrationOptions{})
    if err != nil { return nil, err }
    server := xhttp.Newx402HTTPResourceServer(
        xhttp.RoutesConfig{"GET /api/data": route},
        foundation.WithFacilitatorClient(facilitator),
    )
    for _, registration := range registrations {
        server.Register(registration.Network, registration.Server)
    }
    if err := server.Initialize(ctx); err != nil { return nil, err }
    return nethttp.PaymentMiddlewareFromHTTPServer(
        server, nethttp.WithSyncFacilitatorOnStart(false),
    )(handler), nil
}

This protects GET /api/data; unmatched routes still reach your handler without payment. Initialize before starting your HTTP server. The explicit initialization returns configuration errors to your application instead of delegating startup error handling to the middleware. Supply a startup context with a deadline. The middleware buffers handler responses and is not a streaming adapter. It does not settle ordinary authorization payments after a handler error or panic, and it withholds successful content if settlement fails. It cannot undo work your handler already performed, so make side effects idempotent using the payment identifier where appropriate.

New performs no requests. Config loads configuration on demand and caches it for one hour; RefreshConfig forces a refresh. SignerAddresses uses the supported-capabilities cache and matches an exact network before its namespace wildcard. RefreshSupported refreshes that cache. Concurrent loads share a request; cancelling a joining caller stops only its wait. Cancelling the caller that started the request fails that shared load, and another call can try again. Returned configuration is independent of the cache. Refreshing configuration does not rebuild an existing middleware instance: rebuild its offers and registrations when adopting changes.

Accepts constructs payment offers without sponsorship declarations. Route also checks sponsorship eligibility. PriceSpec.Amount accepts $0.01, 0.01 USDC, or 0.01 with an explicit Currency. Currency overrides a currency in the amount string. USD selects all configured stablecoins for balance and blockchain offers. Instrument offers require explicit Schemes: []string{"instrument"} and a USD price. They accept USD 0.50–92233720368547758.07 in whole cents; their wire amount retains the scale advertised by the Seller configuration. They do not expand USD into stablecoin offers. Amounts allow at most eight decimal places; conversion rejects nonzero precision loss. Schemes and Networks filters intersect; nil permits the default schemes (not instrument or metered upto), while an empty slice selects nothing. The default payment lifetime is 300 seconds.

Metered payments

Metered upto payments require explicit selection. Import the upstream implementation only in applications that need it; fixed-price sellers do not compile Ethereum packages through InFlow's seller package. This is a Go setup difference from Node's dynamically loaded optional EVM package.

import upto "github.com/x402-foundation/x402/go/v2/mechanisms/evm/upto/server"

offers, err := client.Accepts(ctx, seller.AcceptsOptions{
    Price: seller.PriceSpec{Amount: "0.10 USDC"},
    Schemes: []string{"upto"},
})
if err != nil { return err }
registrations, err := client.SchemeRegistrations(ctx, seller.RegistrationOptions{
    Schemes: []string{"upto"},
    MeteredScheme: upto.NewUptoEvmScheme(),
})
if err != nil { return err }

Use offers as the route's Accepts, and register the returned schemes as in the full example. Check for no offers before starting. Configuration must advertise the asset's Permit2 capability and the network's metered proxy and facilitator address. Selecting an available metered scheme without supplying MeteredScheme returns an error rather than silently skipping its registration.

The advertised price is the buyer's authorized maximum. In your handler, call nethttp.SetSettlementOverrides(w, &foundation.SettlementOverrides{Amount: "123"}) before writing the response to charge 123 atomic units of the selected asset. Choose an integer amount between zero and the authorized maximum; without an override, settlement uses that maximum. This path uses an external blockchain wallet, not InFlow-managed Permit2 signing.

Payment response caching

The upstream x402 HTTP middleware owns payment-response cache headers. In x402 Go v2.28.0, its helper can mistake private inside a quoted extension value such as example="a, private, b" for a real directive, leaving a receipt response without the intended private cache policy. See upstream issue #3747. Explicitly include an unqualified private directive in Cache-Control on affected handler responses. InFlow does not replace the upstream cache parser.

Sponsorship and facilitator access

Set RouteOptions.AssetTransferMethod to "permit2" to select compatible Permit2 offers. Balance offers remain available unless filtered out. Route declares EIP-2612 sponsorship only when every Permit2 offer supplies the required token metadata and the refreshed facilitator capabilities agree. Otherwise it checks explicit InFlow EIP-7702 sponsorship support. Missing capability information never implies support. Use separate routes for incompatible tokens. For multiple facilitators, put InFlow first for routes whose sponsorship it advertises; the upstream middleware selects the first facilitator claiming a scheme/network pair.

NewFacilitator requires an API key and implements upstream FacilitatorClient directly. NewAnonymousFacilitator explicitly sends no credentials, even if options contain them. Anonymous facilitation does not load Seller configuration and cannot settle InFlow balance payments. Verify and Settle accept the upstream interface's JSON byte slices. Verification does not settle. False verification or settlement results remain false results; unrelated HTTP failures remain inflow.APIError. Only the recognized Permit2 allowance response is normalized from HTTP 412 into a verification result.

The facilitator preserves a valid payment identifier or derives one from the transaction ID, serialized transaction or signature. If none exists, it hashes the compact JSON payment data; retain the same payload bytes for verification and settlement in that fallback case. Requests preserve unknown payload fields and extensions. Only HTTP 409 idempotency_pending retries: five total attempts, reusing the same request, with a cancellable delay of up to five seconds. Other failures do not trigger automatic payment retries. Cancellation stops waiting; it does not prove that an already submitted payment was reversed.

Shared configuration and errors

The protocol clients accept inflow.Options. Set Environment: inflow.Sandbox for testing; the default is production. BaseURL overrides the environment address for a private deployment or local testing. Configure one of APIKey, an APIKeyProvider callback, or an AccessToken callback. Omit all three for anonymous requests to endpoints that permit them. APIKeyProvider returns the API key to send in X-API-Key; its result is not cached. The callback receives the request context, runs for each attempt, and must support concurrent calls and cancellation. Its errors return unchanged to the caller.

Timeout defaults to 30 seconds per attempt, including token retrieval and response-body reading. An earlier deadline on the operation's context takes precedence. Custom transports must honor that context and must not follow redirects; the SDK does not forward requests to redirect targets. API request and response bodies are limited to 8 MiB.

Use errors.As to inspect *inflow.APIError for the server's error code, message, HTTP status, request identifier, and diagnostic response. Credential headers and recognized credential fields are redacted from error diagnostics. Transport failures have HTTP status zero; cancellation and deadlines support errors.Is. Error diagnostics can still contain application data and are not a substitute for an application's logging policy.

HTTP request methods are internal. Protocol operations choose their retry policy explicitly; the shared transport performs no retries by default. Cancelling a request context stops local work, but does not itself cancel a server-side approval or reverse a payment.

MPP integration design

The integration uses github.com/tempoxyz/mpp-go protocol primitives, with InFlow-owned HTTP handling and payment lifecycle orchestration. It does not fork the upstream library.

Payment data and codecs

Import github.com/inflowpayai/inflow-go/mpp to read payment challenges, credentials, and receipts. For a protected resource's response, pass response.Header.Values("WWW-Authenticate") to mpp.ParseChallenges. It accepts repeated and combined headers, preserves challenge order, and ignores unrelated authentication schemes such as Bearer. A malformed Payment challenge returns *mpp.CodecError; it is not silently removed from the result.

Challenge.Request and Challenge.Opaque retain the issuer's encoded strings. Keep those values unchanged when echoing a challenge in a credential: decoding and re-encoding them can change the seller's challenge binding. Optional challenge fields use pointers so omission and an explicitly empty value remain distinct.

request := mpp.ChargeRequest{
    Amount: "10.5",
    Currency: "USDC",
    MethodDetails: &mpp.InflowMethodDetails{Rail: "balance"},
}
if err := request.Validate(); err != nil {
    return err
}
encodedRequest, err := mpp.Encode(request)

ChargeRequest, SubscriptionRequest, TempoRequest, and TempoPayload provide native shape validation. This does not establish that an account supports a currency or rail, or that a proof is valid. Amounts are strings: InFlow amounts use decimal units; Tempo amounts use integer base units. Request encoding sorts keys and omits null object members, matching InFlow's request encoding. Use strings for exact monetary values; general numeric inputs use binary64 semantics.

DecodeCredential and EncodeCredential preserve payload values, including nulls and large integers decoded as json.Number. DecodeReceipt and EncodeReceipt preserve InFlow settlement fields and arbitrary top-level method extensions in Receipt.Extensions. Extensions cannot overwrite the receipt's named fields. These functions take or return the base64url value alone, without a Payment prefix. Decoding checks structure, not payment validity or settlement.

Codecs accept at most 64 KiB of encoded data per value or header. Errors identify the invalid artifact without copying credential contents into the message. The MPP package compiles only the upstream protocol-primitives package and the Go standard library; it does not import the upstream server, blockchain clients, or Redis integration.

Managed buyer credentials

Use mpp/buyer to obtain a credential for a seller's challenge. The client supports InFlow charge and subscription challenges and Tempo charge challenges. Fulfil and Prepare send payment requests to InFlow; they do not sign locally or send the credential to the seller's resource.

client, err := buyer.New(buyer.Options{
    Options: inflow.Options{
        Environment: inflow.Sandbox,
        APIKey: os.Getenv("INFLOW_API_KEY"),
    },
})
if err != nil {
    return err
}
credential, err := client.Fulfil(ctx, challenge, buyer.PaymentOptions{})

Import github.com/inflowpayai/inflow-go/mpp/buyer alongside the root inflow package. The challenge comes from mpp.ParseChallenges. Keep its encoded request and opaque fields intact. For an InFlow instrument charge, PaymentOptions.InstrumentID selects an owned card; leave it empty to use the primary card. A rejected selection fails without choosing another card. For access under an existing InFlow subscription, supply PaymentOptions.SubscriptionID; that calls subscription authorization instead of creating another purchase. Tempo requires no per-call selector.

For a separate creation and waiting step, call Prepare, then payment.Wait(ctx) or payment.Cancel(ctx). TransactionID() and ApprovalID() expose the initial response identifiers. Concurrent waits share one polling sequence and result; each successful wait receives its own decoded credential. Cancelling a wait abandons that payment for every waiter, not other payments using the same client. A completed result remains available on the handle.

PollInterval defaults to five seconds; the server's retryAfterSeconds takes precedence, including zero. WaitTimeout defaults to fifteen minutes after creation returns. It includes time before Wait and time spent making polling requests. The context passed to Prepare owns the operation; keep it alive until waiting or cancellation finishes.

When an unfinished payment fails or is cancelled, the SDK attempts cancellation of its known approval and waits up to five seconds for that attempt. This cleanup uses a separate context so an already-cancelled payment context does not prevent it. Cleanup failure never replaces the payment error. Explicit Cancel returns the cleanup error, or its own caller context error if that caller stops waiting. No approval can be cancelled when creation ends without receiving its identifier. Cancelling subscription authorization does not cancel the subscription, and cancelling a completed payment does not reverse it.

Use errors.As with *buyer.Error for payment failures. Its Code distinguishes cancellation, pending timeout, platform rejection, expiry, malformed responses or credentials, and unsupported methods. Problem retains the platform's problem JSON. Cancellation and timeout support errors.Is with the corresponding context error. HTTP failures retain *inflow.APIError. Creation, authorization, polling, and cleanup requests make one attempt each; the SDK does not replay a payment workflow after an uncertain network outcome.

Payment-aware HTTP requests

client.Do(request, paymentOptions) sends a resource request, handles a 402 by fulfilling the first supported MPP challenge in the server's order, and sends one paid retry. Use Fulfil or Prepare when your application needs to choose a particular challenge itself. Close the returned response body, just as with http.Client.Do. InFlow API authentication is not sent to the resource.

request, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil)
if err != nil {
    return err
}
request.Header.Set("X-AEP-API-Key", serviceAPIKey)
response, err := client.Do(request, buyer.PaymentOptions{})
if err != nil {
    return err
}
defer response.Body.Close()

MPP sends its credential in Authorization: Payment .... If a 402 request already has a nonempty Authorization header, or URL credentials that produce Basic authentication, Do returns ErrAuthorizationConflict before obtaining a payment credential. Separate API-key and cookie headers are preserved. Ordinary non-402 responses, including authenticated ones, pass through without a payment attempt. The SDK does not move application credentials to another header.

A request with a body must supply a working GetBody before payment starts. http.NewRequest provides it for strings.Reader, bytes.Reader, and bytes.Buffer inputs. The SDK does not buffer an arbitrary streaming body. It closes an intermediate 402 body without draining it and does not modify the caller's headers or GetBody. The resource transport is Options.Transport; Options.Timeout applies to each resource exchange, including response-body reading.

Neither the initial nor the paid request follows redirects. A paid response of 401, 402, or another failure status is returned as-is; it does not initiate another payment. A network failure after submitting a credential can leave the payment outcome unknown. The SDK does not retry that request or attempt to reverse payment. Inspect the result before retrying in your application. Payment-Receipt, when supplied by the seller, remains on the response for decoding with mpp.DecodeReceipt.

Accepting payments

Import github.com/inflowpayai/inflow-go/mpp/seller and create a client with an API key from an InFlow Seller account: Sandbox for testing or Production for live payments. A Developer key does not authorize Seller configuration, validation, or broadcast.

client, err := seller.New(inflow.Options{
    Environment: inflow.Sandbox,
    APIKey: os.Getenv("INFLOW_API_KEY"),
})
if err != nil {
    return err
}
handler, err := client.Protect(seller.Route{
    Realm: "api.example.com",
    SecretKey: os.Getenv("MPP_SECRET_KEY"),
    Offers: []seller.Offer{{Charge: &mpp.ChargeRequest{
        Amount: "0.01",
        Currency: "USDC",
    }}},
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("paid response"))
}))
if err != nil {
    return err
}
mux.Handle("/paid", handler)

Use a private, high-entropy MPP_SECRET_KEY, separate from the InFlow API key. Keep the same signing key across instances serving the same routes. Changing it invalidates outstanding challenges. Realm identifies the protected service; Lifetime defaults to five minutes.

An Offer selects exactly one of Charge, Subscription, or Tempo. Use multiple offers to advertise alternative prices or payment methods. InFlow offers obtain their recipient from the Seller account and select an advertised currency/rail combination. If multiple rails are available, set MethodDetails.Rail; required instrument identifiers must also be provided. Subscription requests include their recurring terms. Tempo requests specify the token address and recipient; their method details default to feePayer: false and supportedModes: ["pull"].

Protect returns an ordinary http.Handler. Without a payment it returns a 402 challenge. It verifies the echoed challenge signature, expiry, realm, opaque data, and configured offer before requesting platform validation or broadcast. It then sets Payment-Receipt and runs the application handler. Payment happens before the handler: a handler failure does not reverse it. The response is not buffered, so the handler can stream normally after payment succeeds.

If validation or broadcast returns a payment problem with an HTTP error status, Protect returns that status and Problem Details body with Cache-Control: no-store. It does not serve paid content, emit a successful receipt, or offer a new purchase. For example, pending settlement can return 503; keep the original transaction and credential when checking its status or retrying. Malformed problems and other verification errors return a generic 402 challenge without exposing internal errors.

Instrument receipts must identify the inflow method and the original challenge. A mismatched receipt prevents the paid handler from running; it does not reverse a payment or authorize a retry.

Paid responses include Cache-Control: private so shared caches must not reuse them for other users. Protect preserves your handler's other cache directives and adds private when necessary. This does not prohibit browser-local caching; set Cache-Control: no-store in your handler when responses must not be stored.

Place application authentication outside this handler. MPP uses Authorization: Payment ...; separate API-key or cookie authentication avoids conflicting with that header. CanOffer filters which configured offers are advertised. It is not access control and does not revoke credentials already issued for a matching offer. Optional Opaque is an encoded value signed into the challenge; use distinct values when otherwise identical offers must not be interchangeable across routes. The middleware does not bind the HTTP request body to payment; enforce application-specific request authorization in your own handler or middleware.

For custom integrations, Prepare resolves a typed offer without minting a challenge. Validate performs the non-consuming platform check, Broadcast performs the terminal payment operation, and Verify validates then broadcasts. These direct methods do not establish that a credential belongs to your HTTP route: that local signature and route check belongs to Protect, or to your own protocol integration. Inspect *seller.Error for capability failures or rejected payment; Problem preserves the platform's problem JSON. Transport and account-role failures remain *inflow.APIError.

Configuration loads on demand. Load(ctx) performs an explicit startup check; concurrent callers share the active load and successful results remain cached. A failed load permits a later attempt. The caller initiating a shared load owns its request context; cancellation can fail that shared attempt, while another call can retry. Cancelling a waiting caller does not cancel the owner's load.

Configuration and validation allow up to three transient retries. Broadcast retries are enabled only when configuration advertises idempotency keys, and reuse one key throughout that operation. Pass a stable key to Broadcast when explicitly retrying an uncertain outcome; an empty key generates a fresh one. Without advertised idempotency support, broadcast makes one attempt. Never retry the entire protected application request solely because its response was lost.

Accepting Stripe Shared Payment Tokens

Use seller.Offer{Stripe: &seller.StripeOffer{Amount: "1.25"}} with the same Prepare and Protect APIs. Amount is a decimal USD string: the SDK converts "1.25" to "125" cents without rounding. Supported prices range from USD 0.50 to 999999.99.

Your InFlow Seller account must have Stripe connected and advertise Stripe charge support. The authenticated Seller configuration supplies the business profile and accepted payment methods (such as card and Link). The application does not supply a Stripe secret key or override those settings. See the runnable Stripe example.

An external Buyer supplies a Shared Payment Token. The InFlow Go Buyer does not create these tokens; this is Seller acceptance, not Buyer token creation or subscription support. A payer need not have an InFlow identity. The SDK preserves a supplied source and sends an empty source when it is omitted, as required by the InFlow API.

ExternalID is optional; use a pointer to distinguish an omitted reference from an empty one. When supplied by the Seller, the credential must repeat it exactly. Metadata allows up to 45 string entries, with nonblank keys of at most 40 characters and values of at most 500. Keys cannot contain brackets or use the reserved names externalId, inflowMppTransactionId, mppChallengeId, mppIntent, mppMethod, or stripeNetworkProfile. Description and Recipient are optional request fields; they do not replace the configured Stripe business profile.

Protect checks the signed challenge and route terms before forwarding the token to InFlow. Validation does not consume the payment; broadcast performs processing. Only a successful receipt for the same Stripe challenge releases the handler. Pending processing and rejected or mismatched receipts remain failures. The SDK neither decrypts tokens nor calls Stripe directly.

The released mpp-go v0.2.0 has no Stripe method package. InFlow Go supplies this method through its existing Seller integration; it does not depend on an upstream Stripe implementation or add a Stripe library. General MPP transport and lifecycle differences are listed below.

Visa CARD payments

CARD is a one-time USD payment using an encrypted Visa credential. It is not a Stripe Shared Payment Token, nor the inflow instrument rail. The Seller accepts it with:

offer := seller.Offer{Card: &seller.CardOffer{Amount: "1.25"}}

Pass this offer to Prepare or Protect. The price is decimal USD, from 0.50 to 999999.99; the wire challenge contains integer cents. Authenticated Seller configuration must advertise CARD and supplies the merchant, recipient, accepted networks and public encryption key. Route options cannot replace these settings. Description, ExternalID and BillingRequired are optional pointers; an empty reference and an explicit false billing requirement are preserved.

The Buyer supplies the purchase merchant and optionally a linked card:

response, err := client.Do(request, buyer.PaymentOptions{
    Merchant: &buyer.CardMerchant{
        Name: "Example Store", URL: "https://store.example", CountryCode: "US",
    },
    InstrumentID: linkedCardID,
})

Supplying Merchant selects CARD offers rather than falling back to another payment method. Omit InstrumentID to use the account's primary instrument. InFlow checks ownership, Visa eligibility and a valid USD allowance; the SDK does not select another card when those checks fail. The merchant context does not override the signed challenge or registered merchant details. Prepare and Fulfil accept the same options. CARD does not support subscription authorization.

The Buyer obtains an encrypted credential through the ordinary approval and polling flow, checks the complete returned challenge, and forwards the credential unchanged. Readiness is not settlement. The Seller checks its signed route terms, validates through InFlow, then broadcasts; only a successful CARD receipt for that challenge permits delivery. Neither side decrypts the payload. Optional billing fields, extensions and source are preserved. An omitted source becomes an empty string only in the Seller's platform request, so an external payer does not need an InFlow identity.

See the CARD Seller example and CARD Buyer example. Released mpp-go v0.2.0 has no CARD method package; InFlow Go implements this method in its existing MPP clients, without adding a card-processing dependency. Server-side credential verification and settlement remain authoritative.

Upstream compatibility notes

These observations apply to mpp-go v0.2.0 (source revision). They distinguish the library's general-purpose behavior from the requirements of the InFlow MPP integration. Recheck them when upgrading the dependency.

  1. Preserving application authentication. The upstream buyer transport puts the payment credential in Authorization, replacing an existing value. An authenticated resource may already use that header for its application session. InFlow's HTTP integration rejects that conflict before payment and preserves separate cookie/API-key authentication. It does not invent an alternate payment header. Source.

  2. Checking request replay before payment. The upstream transport creates a payment credential before checking whether the request body can be replayed. For a body without GetBody, this can invoke the payment method and then fail locally without sending the paid request. InFlow checks that it can recreate the body before requesting payment. Supply Request.GetBody; http.NewRequest supplies it automatically for strings.Reader, bytes.Reader, and bytes.Buffer. Source. Tracked in upstream issue #181.

  3. Separate validation and broadcast. The upstream seller Intent interface exposes one Verify operation returning a receipt. InFlow's Validate checks a credential without consuming payment; Broadcast submits it for payment processing. Keeping them separate lets an application check a credential without unexpectedly processing a payment. Use InFlow's Verify to perform both, or Protect to perform both before running an HTTP handler. Source. Upstream PR #137 provides a split lifecycle on main; it is not included in the pinned v0.2.0 release.

  4. Subscription entry points. The upstream generic verification function accepts an arbitrary intent, but the Charge helper selects charge explicitly, and ComposeMiddleware operates on charge configurations. This limits those convenience APIs, not the protocol's ability to encode subscription challenges. InFlow sellers advertise subscriptions with seller.Offer.Subscription. Buyers use Fulfil to purchase one, or supply PaymentOptions.SubscriptionID to authorize access under an existing subscription. Charge source, composition source.

  5. Preserving receipt fields. The upstream receipt type has a fixed set of fields and a nested extra object. InFlow receipts also carry top-level fields such as challengeId, subscriptionId, and settlement. The upstream receipt parser/formatter does not preserve those top-level fields; moving them into extra changes the wire format. Use InFlow's DecodeReceipt and EncodeReceipt to preserve them. Type, parser and formatter. Upstream PR #115 preserves method-defined receipt fields on main; it is not included in the pinned v0.2.0 release.

  6. Server dependency coupling. The upstream server package imports its Tempo package, which brings Ethereum, Tempo, and Redis packages into the compilation dependencies. InFlow delegates payment processing to its platform and does not need that entire server implementation for this purpose. Using the protocol primitives avoids this coupling. The upstream module's web-framework requirements do not mean every framework is compiled into every consumer. This distinction was checked with go list -deps. Server imports, Tempo Redis dependency.

  7. MPP over MCP is not supported. The released Go library has no MCP integration package corresponding to the mppx/mcp/client integration used by InFlow Node. InFlow Go does not implement an independent MPP-over-MCP transport. Support depends on an upstream implementation so that integrators do not adopt an InFlow-specific design that could conflict with the upstream protocol integration. This limitation concerns MPP, not x402's separate MCP integration. Released package tree. Tracked in upstream issue #183.

  8. Preserving opaque challenge data. The upstream challenge parser decodes opaque into a string map, and its formatter encodes that map again. An issuer's original encoded value can therefore change. InFlow keeps that field as its original string and uses its own wire types, while reusing compatible upstream header primitives. Parser and formatter. Tracked in upstream issue #182.

Development

Requires Go 1.26 or later and Make. Run make verify for formatting, module tidiness, compilation, static analysis, race-enabled tests, package documentation, and a build from a separate consumer module. make format formats Go source files.

CI runs on Go 1.26 and 1.27. Local tests require at least 99% statement coverage in every source file. Codecov evaluates 99% project and changed-line targets without tolerance. Its line-based measurements differ from Go's statement coverage; aim for 100% on both.

The consumer check builds and runs a separate Go module against the local checkout. It checks configuration and error types independently of payment behavior.

Node interoperability

The Node interoperability workflow runs real HTTP exchanges in both directions: a Go Buyer against Node Seller middleware and a Node Buyer against Go Seller middleware. It covers MPP charges, subscription purchases, existing-subscription authorization, Tempo charges, and x402 balance and exact payments. Cases include success, pending approvals where applicable, rejected validation, failed settlement, and failed application handlers. Assertions check payment data, receipts, platform credential isolation, and handler/settlement ordering.

The payment platform is a loopback simulator. These tests do not sign real transactions, move money, or certify blockchain settlement. The Node MPP fetch client is configured for one payment attempt (maxPaymentRetries: 1); this prevents its automatic retries from purchasing again after a rejected paid request. Existing-subscription authorization returns a credential directly and does not have a pending-purchase case.

For a local run, use Node 24 and a clean inflow-node checkout at the revision in interop/node.lock.json. In that checkout run pnpm install --frozen-lockfile and pnpm build, then run this command from the Go repository:

node scripts/interoperability.mjs /path/to/inflow-node /tmp/inflow-interoperability.json

Choose an unused report filename; the runner refuses to overwrite an existing report. Reports record SDK revisions, toolchain versions, Go dependencies, and each case's platform requests. Hosted reports are available from the workflow's artifacts. The peer programs live in interop/ and are test tooling, not SDK packages intended for application use.

Releases

Go versions come from immutable Git tags such as v0.1.0; there is no separate version constant to update. Install a release with go get github.com/inflowpayai/inflow-go@v0.1.0, replacing the example version with the release you want. All packages in this repository share that version. The SDK user-agent reads the installed module version. Local replace builds report devel.

Maintainers use Actions → Release:

  1. Select Run workflow, choose main, and enter a stable version without v.
  2. Leave dry_run checked. The workflow runs the repository checks, a separate local consumer, shared conformance, and Node interoperability. Download inflow-go-release-evidence from the completed run to inspect the reports. A dry run does not create a tag or GitHub release.
  3. After reviewing the evidence and approving publication, run the workflow from main with the same version and dry_run unchecked. It verifies the selected commit again, creates an annotated tag, installs that version through proxy.golang.org in a separate consumer module without a local replacement, and publishes the GitHub release with its verification reports.

No registry account, API key, or repository secret is required. The workflow uses GitHub's provided token. Merging a PR or pushing a tag does not trigger publication. Release-workflow PRs exercise the dry-run path automatically, using 0.1.0 only as a validation example.

Before version 1.0, use a minor increment for incompatible public API changes and a patch increment for compatible fixes. From version 1.0, use semantic versioning. This module path supports major versions 0 and 1; version 2 requires a /v2 module path and a separate migration.

If publication fails after creating the tag, keep the tag: consumers may already have downloaded it. Re-run the failed workflow jobs on the same commit after resolving the failure. The workflow accepts an existing tag only when it points to that exact commit and refuses to replace an existing GitHub release. If the code must change, choose a new version. Go proxy propagation can delay the published-consumer check; do not delete or move a tag to retry it.

Release evidence records the source commit, contract and Node revisions, toolchain, dependency versions, report checksums, and workflow run. These are traceability records, not signed build attestations. The tests simulate payment processing and do not certify live settlement.

About

Go SDK for InFlow MPP and x402 Buyer and Seller integrations.

Topics

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages