paybondpaybond
Sign in

Fund intents on Stripe MPP

Beta Preview guide to funding Paybond intents on stripe_mpp: charge vs Tempo session selection, Payment Auth header flow, sandbox behavior, and tenant-isolation boundaries.

Beta Preview. stripe_mpp is Paybond's Machine Payments Protocol settlement rail. Harbor advances funding on POST /harbor/intents/{id}/fund with HTTP Payment Auth semantics — 402 challenges, app-owned credentials, and Payment-Receipt on success.

This guide is the deep dive for stripe_mpp only. For the full multi-rail create → fund → capability flow, see Fund intents by rail. For tenant-admin rail setup, see Configure settlement rails.

Prerequisites

  • Tenant admin has added stripe_mpp to allowed rails and MPP readiness shows ready or configured in Configuration → Settlement.
  • Application code uses Kit with tenant-derived API keys — never client-supplied tenant ids.
  • Intent budgets are USD-denominated (currency: "usd"). Harbor rejects non-USD stripe_mpp intents until multi-currency policy ships.
  • Production enablement requires Harbor MPP runtime readiness and a linked Stripe destination. Use x402_usdc_base or stripe_connect for general production agent spend until your tenant passes MPP readiness checks.

Charge vs session — which helper?

MPP exposes two funding modes on the same rail literal. Pick the Kit helper that matches your spend shape — the helper selects the correct WWW-Authenticate: Payment … challenge.

ModeWhen to useTypeScriptPythonChallengeFunded when
ChargeOne-shot purchase, single tool call, fixed budget up frontfundWithMppChargefund_with_mpp_chargeintent="charge", method="stripe"funding.status = charge_succeeded
SessionStreaming metering — many tool calls under one pre-funded Tempo channelfundWithMppSessionfund_with_mpp_sessionintent="session", method="tempo"funding.status = deposit_confirmed

Charge maps to a Stripe-backed one-shot authorization (PaymentIntent / SPT-style credential). Harbor verifies the credential, confirms the charge, then mints capability_token.

Session opens a Tempo v2 payment channel: one Paybond intent maps to one channel. Harbor verifies the open-session credential, confirms the TIP-1034 reserve deposit on-chain when PAYBOND_TEMPO_RPC_URL is configured (sandbox Moderato or production mainnet), opens the channel, then mints capability_token. Subsequent tool spend consumes cumulative vouchers under the existing Paybond guardrail lifecycle — vouchers do not move the intent to released; evidence and settlement confirmation still gate payee payout.

Do not mix helpers on the same intent

Call one helper per intent. fundWithMppCharge ignores session challenges; fundWithMppSession ignores charge challenges. If you need streaming metering, create the intent with session funding from the start — do not fund with charge and later attempt session top-ups on the same intent without an explicit amendment policy.

Decision guide

Your workflowRecommended mode
Book one hotel, pay one invoice, single API purchaseCharge
Agent runs dozens of metered tool calls against one budget ceilingSession
Sandbox rehearsal of Payment Auth transport onlyCharge (simplest path)
Sandbox rehearsal of Tempo channel + voucher meteringSession (requires Tempo recipient configured)

For protocol background and Harbor state mapping, see MPP rail evaluation.

Payment Auth header flow

Harbor never marks an MPP intent funded on create alone. Funding is a two-step (or poll) handshake on the same route.

Step-by-step

StepHTTPRequest headersResponse headersBody
1. ChallengePOST /harbor/intents/{id}/fundAuthorization: Bearer <api_key>402 + one or more WWW-Authenticate: Payment …; Cache-Control: no-storefunding.status = payment_required; funding.intent, method, challenge_id
2. CredentialPOST /harbor/intents/{id}/fund (retry)Authorization: Bearer <api_key> and x-paybond-payment-authorization: Payment <credential>200 + Payment-Receipt: …; Cache-Control: private or 202 (session pending) or 402 + application/problem+jsonstate = funded, capability_token on success

Gateway vs direct Harbor

CallerCredential headerWhy
Kit / Gateway (POST /harbor/intents/{id}/fund)x-paybond-payment-authorization: Payment …Keeps Authorization: Bearer free for tenant API key auth
Direct Harbor (internal)Authorization: Payment …Standard Payment Auth scheme on the upstream request

Gateway translates x-paybond-payment-authorizationAuthorization: Payment when forwarding to Harbor. Response headers (WWW-Authenticate, Payment-Receipt, Cache-Control, x402-compat payment-required / payment-response) pass through unchanged.

Credential ownership

createPaymentCredential / create_payment_credential stays app-owned. Paybond never stores MPP wallet keys, SPT secrets, or Tempo signing material. Your callback receives the selected WWW-Authenticate challenge and returns a Payment Auth credential string.

Invalid credentials

When Harbor rejects a credential, expect:

  • 402 Payment Required
  • Content-Type: application/problem+json (RFC 9457 problem details)
  • Fresh WWW-Authenticate challenge(s)
  • Cache-Control: no-store

Fix the credential and retry with a new challenge — do not treat 402 + problem body as funded. Reuse the same idempotency-key and empty JSON body {} on safe retries.

Session deposit pending (202 Accepted)

Live Tempo session funding may return 202 Accepted when the open transaction is broadcast but the TIP-1034 reserve deposit is not yet observable on-chain:

  • state remains open
  • funding.status = session_open_pending
  • funding.channel_id and last_settle_tx_hash are persisted for recovery
  • Kit helpers poll /fund until capability_token is present or a terminal failure surfaces (PaybondMppFundingPendingError when polling exhausts)

Harbor's background recovery worker also reconciles session_open_pending intents by re-querying the Tempo RPC and minting the capability token when the deposit confirms. Poll with fresh recognition proofs; you do not need to resubmit the Payment Auth credential on each poll once Harbor has accepted the open credential.

Recognition proofs

Each /fund call needs a fresh AgentRecognitionProofV1 with purpose: "harbor.intent.fund" bound to the tenant and request envelope. Kit helpers issue proofs through your issueRecognitionProof callback. Proof nonces are single-use — credential retries need their own recognition proof.

Sandbox behavior

Hosted sandbox tenants rehearse real Stripe test-mode MPP when Harbor has PAYBOND_STRIPE_MPP_TEST_SECRET_KEY configured. Tenants self-configure a profile_test_* Stripe MPP profile id in Settlement settings before funding.

BehaviorSandboxProduction
Stripe charge pathStripe test-mode via Harbor PAYBOND_STRIPE_MPP_TEST_SECRET_KEY + tenant profile_test_*Stripe live-mode via STRIPE_SECRET_KEY + tenant profile_*
Tempo deposit confirmationOn-chain when PAYBOND_TEMPO_RPC_URL points at Moderato (42431); otherwise session open may stay session_open_pending until RPC is configuredOn-chain TIP-1034 getChannelState via PAYBOND_TEMPO_RPC_URL (mainnet 4217)
MPP profile & Tempo recipientTenant-configured in Settlement settings (profile_test_*, optional mpp_tempo_recipient_address); masked in consoleTenant-configured in Settlement settings; masked in console
First /fund without credentialMay return dual WWW-Authenticate headers (charge and session) only when mpp_session_enabled = true and mpp_tempo_recipient_address is configuredSame gate — session challenge appears only when session mode is enabled
Capability tokenMinted after Stripe test charge or Tempo deposit confirmationMinted after live charge or on-chain deposit confirmation

Dual challenges. When mpp_session_enabled is true and a Tempo recipient is configured, the first charge-mode /fund without a credential may return both charge and session WWW-Authenticate values. Kit selects automatically:

  • fundWithMppChargeintent="charge", method="stripe"
  • fundWithMppSessionintent="session", method="tempo"

Harbor's simulated MPP runtime (PAYBOND_SETTLEMENT_MODE=simulated) is for local unit tests only — not the default hosted sandbox path.

For the fastest sandbox path without writing MPP wallet code, use paybond.guardrails.bootstrapSandbox(...) to get a pre-funded intent — see One-command guardrails.

Offline Kit tests can use paybond dev loop --offline and WireMock mappings; live Stripe sandbox MPP and Tempo testnet checks run only behind explicit env flags — never in default make all.

Kit integration

One-shot charge (TypeScript)

const created = await paybond.intents.create({
  operation: "saas.api.purchase",
  requestedSpendCents: 5_000,
  currency: "usd",
  settlementRail: "stripe_mpp",
  completionPreset: "api_response_ok",
});

const funded = await paybond.intents.fundWithMppCharge({
  intentId: created.intent_id,
  recognitionProof: await issueAgentRecognitionProofV1({
    purpose: "harbor.intent.fund",
    method: "POST",
    path: `/harbor/intents/${created.intent_id}/fund`,
    body: {},
  }),
  createPaymentCredential: (challenge) => mppWallet.createPaymentCredential(challenge),
  issueRecognitionProof: (envelope) =>
    issueAgentRecognitionProofV1({
      purpose: "harbor.intent.fund",
      method: envelope.method,
      path: envelope.path,
      body: envelope.body,
    }),
});

const capabilityToken = funded.capabilityToken;

Tempo session channel (Python)

funded = await paybond.intents.fund_with_mpp_session(
    intent_id=intent_id,
    recognition_proof=fund_recognition_proof,
    create_payment_credential=mpp_wallet.create_payment_credential,
    issue_recognition_proof=issue_fund_recognition_proof,
)
capability_token = funded.capability_token

Prefer these helpers over manual /fund loops — they parse challenges, invoke your credential callback, set paymentAuthorization, poll pending states, and stop on terminal failures (PaybondMppFundingFailedError / PaybondMppFundingPendingError in TypeScript).

Funding response fields

After challenges progress, inspect funding on the fund result:

FieldChargeSessionMeaning
intent, method, challenge_idActive Payment Auth challenge metadata
settlement_asset, settlement_networkDenomination Harbor snapshots (USDC on Tempo for MVP)
deposit_amount_base_unitsUSD cents × 10 000 for Tempo USDC base units
session_protocol, channel_idTempo v2 channel identity
accepted_cumulative_base_units, pending_cumulative_base_unitsVoucher metering state
descriptor_hash, channel_statusChannel integrity and lifecycle

See SDK reference — IntentFundingResult and Harbor MPP handshake.

Session voucher metering at verify

After session funding, the intent stays funded while the agent runs metered tool calls. Harbor does not advance the intent to released on each voucher — terminal payout still requires evidence submission and settlement confirmation.

For stripe_mpp session intents, POST /harbor/verify (Gateway POST /verify) performs capability checks and accepts a Tempo voucher credential when metering spend inside the open channel.

Verify handshake (session intents only)

StepHTTPRequest headersResponse
1. ChallengePOST /verifyAuthorization: Bearer <api_key> + verify JSON body200 with allow: false, code: mpp_voucher_required, WWW-Authenticate: Payment … (intent="session", method="tempo", scope: verify_voucher in opaque metadata); Cache-Control: no-store
2. CredentialPOST /verify (retry)Same body and x-paybond-payment-authorization: Payment <voucher-credential>200 with allow: true when capability + voucher pass; Payment-Receipt on success; Cache-Control: private
Rejected voucherPOST /verifyCredential present but invalid or over budget200 with allow: false, code: mpp_voucher_rejected, fresh WWW-Authenticate; no pending_cumulative_base_units mutation

Charge-mode MPP intents skip the voucher path — capability verification alone is sufficient after one-shot funding.

Budget cap and cumulative vouchers

Harbor enforces the signed intent budget (amount_cents, converted to USDC base units as amount_cents × 10 000) at voucher acceptance time:

  • Each voucher carries a monotonic cumulative spend total for the Tempo channel.
  • Harbor rejects vouchers whose cumulative total exceeds the intent budget, even when the session deposit ceiling is higher.
  • Only one pending voucher may be in flight per intent. Complete gateway spend finalization (commit or release) before submitting the next voucher credential.
  • Rejected vouchers do not advance accepted_cumulative_base_units or set pending_cumulative_base_units.

Inspect funding.accepted_cumulative_base_units and funding.pending_cumulative_base_units on verify responses and intent detail for metering state. Kit spend guards still enforce per-operation requested_spend_cents against the capability token — vouchers meter rail-internal Tempo spend under the same intent ceiling.

Challenge binding on fund and verify

MPP challenges issued by Harbor include:

ControlBehavior
Challenge TTLExpires at the earlier of intent deadline and a capped window (default 15 minutes, PAYBOND_MPP_CHALLENGE_TTL_MINUTES). Expired credentials return a fresh challenge with payment_expired.
POST body digestChallenges bind SHA-256 of the exact fund or verify request body (base64url). Retries must send the same body bytes used when the challenge was issued.
Opaque tenant/intent bindingCredentials echo an opaque blob tying the challenge to tenant_id, intent_id, and (for verify) scope: verify_voucher.
Challenge ID consumptionEach challenge_id is consumed atomically before settlement side effects. Replayed credentials fail without state mutation.

Empty-body fund requests use the digest of an empty string. Verify requests use the JCS-canonical JSON body Harbor receives on POST /verify.

Tenant isolation — critical boundaries

MPP funding touches payment credentials and channel state. Treat cross-tenant access as a severity-zero defect.

RuleWhy
Never pass tenant_id from unauthenticated user inputKit derives tenant scope from the API key. Harbor rejects mismatched tenant context on every write.
Never supply Stripe profile ids, Tempo recipient addresses, or channel_id in create/fund payloadsDestinations are server-owned from settlement config. Client-supplied routing is ignored or rejected.
Bind recognition proofs to the authenticated tenantverifier_context.tenant_id must match paybond.harbor.tenantId.
One capability token per intentTokens are minted when that intent reaches funded. Reusing across intents fails verify.
Treat tenant/intent mismatch errors as fatalDo not retry HarborHttpError or plain Error messages about tenant or intent mismatch — investigate immediately.
Do not log credentialsNever log API keys, JWTs, x-paybond-payment-authorization values, capability tokens, or wallet signing material.

Harbor security tests enforce: tenant A cannot fund or voucher tenant B's intent or channel; stale or replayed challenge_id and voucher credentials fail without state mutation; vouchers that would exceed the intent amount_cents budget are rejected without pending state.

MPP lifecycle events (challenge_consumed, charge_succeeded, session_opened, voucher pending/commit/release, finalize applied/skipped) are recorded as mpp_provider_event ledger rows for Signal fraud review.

Security boundary

If your agent runtime receives a channel_id or MPP profile id from an LLM or browser client, discard it. Only Harbor-returned opaque fields in authenticated API responses are trustworthy for support debugging — lifecycle logic should branch on state, funded, and funding.status, not client-supplied provider ids.

Integration checklist

Tenant admin (before first MPP fund)

  • Paid plan activated (production) or sandbox tenant selected (rehearsal)
  • Stripe Connect destination linked (MPP routes through the tenant's linked Stripe destination)
  • In Configuration → Settlement, MPP profile configured — paste stripe_mpp_profile_id; readiness shows stripe_mpp_profile_configured
  • For session mode: set Tempo recipient and enable Tempo session funding in Settlement settings (mpp_tempo_recipient_address, mpp_session_enabled)
  • Production Harbor has PAYBOND_TEMPO_RPC_URL when live tenants allow stripe_mpp session funding
  • Hosted sandbox Harbor has PAYBOND_STRIPE_MPP_TEST_SECRET_KEY when sandbox tenants allow stripe_mpp
  • stripe_mpp added to allowed rails

Application developer

  • settlementRail: "stripe_mpp" / settlement_rail: "stripe_mpp" with currency: "usd"
  • Chose charge or session helper before first /fund
  • App-owned createPaymentCredential / create_payment_credential wired
  • Fresh recognition proof per /fund attempt
  • Guard tools only after capability_token is present
  • For session mode: wire voucher credentials on POST /verify (via spend guard) and respect single pending voucher semantics
  • Submit tool-completion evidence after guarded work — not Payment Auth receipts or Stripe PaymentIntent ids

Common mistakes

  • Using fundWithMppCharge when you need streaming vouchers — switch to fundWithMppSession at create time.
  • Sending Authorization: Payment through Gateway — use x-paybond-payment-authorization so Bearer auth still works.
  • Treating 402 + problem+json as success — only 200 + capability_token means funded; 202 + session_open_pending means keep polling.
  • Submitting a second voucher while one is pending — wait for spend completion finalize (commit/release) before the next verify credential.
  • Assuming deposit size overrides intent budget — cumulative vouchers are capped at signed amount_cents, not merely the Tempo channel deposit.
  • Submitting funding payloads as completion evidence — predicates evaluate vendor tool results, not MPP challenge ids.
  • EUR or multi-currency intents on stripe_mpp — rejected at create until policy expands.

Where to go next