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_mppto 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-USDstripe_mppintents until multi-currency policy ships. - Production enablement requires Harbor MPP runtime readiness and a linked Stripe destination. Use
x402_usdc_baseorstripe_connectfor 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.
| Mode | When to use | TypeScript | Python | Challenge | Funded when |
|---|---|---|---|---|---|
| Charge | One-shot purchase, single tool call, fixed budget up front | fundWithMppCharge | fund_with_mpp_charge | intent="charge", method="stripe" | funding.status = charge_succeeded |
| Session | Streaming metering — many tool calls under one pre-funded Tempo channel | fundWithMppSession | fund_with_mpp_session | intent="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 workflow | Recommended mode |
|---|---|
| Book one hotel, pay one invoice, single API purchase | Charge |
| Agent runs dozens of metered tool calls against one budget ceiling | Session |
| Sandbox rehearsal of Payment Auth transport only | Charge (simplest path) |
| Sandbox rehearsal of Tempo channel + voucher metering | Session (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
| Step | HTTP | Request headers | Response headers | Body |
|---|---|---|---|---|
| 1. Challenge | POST /harbor/intents/{id}/fund | Authorization: Bearer <api_key> | 402 + one or more WWW-Authenticate: Payment …; Cache-Control: no-store | funding.status = payment_required; funding.intent, method, challenge_id |
| 2. Credential | POST /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+json | state = funded, capability_token on success |
Gateway vs direct Harbor
| Caller | Credential header | Why |
|---|---|---|
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-authorization → Authorization: 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 RequiredContent-Type: application/problem+json(RFC 9457 problem details)- Fresh
WWW-Authenticatechallenge(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:
stateremainsopenfunding.status=session_open_pendingfunding.channel_idandlast_settle_tx_hashare persisted for recovery- Kit helpers poll
/funduntilcapability_tokenis present or a terminal failure surfaces (PaybondMppFundingPendingErrorwhen 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.
| Behavior | Sandbox | Production |
|---|---|---|
| Stripe charge path | Stripe test-mode via Harbor PAYBOND_STRIPE_MPP_TEST_SECRET_KEY + tenant profile_test_* | Stripe live-mode via STRIPE_SECRET_KEY + tenant profile_* |
| Tempo deposit confirmation | On-chain when PAYBOND_TEMPO_RPC_URL points at Moderato (42431); otherwise session open may stay session_open_pending until RPC is configured | On-chain TIP-1034 getChannelState via PAYBOND_TEMPO_RPC_URL (mainnet 4217) |
| MPP profile & Tempo recipient | Tenant-configured in Settlement settings (profile_test_*, optional mpp_tempo_recipient_address); masked in console | Tenant-configured in Settlement settings; masked in console |
First /fund without credential | May return dual WWW-Authenticate headers (charge and session) only when mpp_session_enabled = true and mpp_tempo_recipient_address is configured | Same gate — session challenge appears only when session mode is enabled |
| Capability token | Minted after Stripe test charge or Tempo deposit confirmation | Minted 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:
fundWithMppCharge→intent="charge",method="stripe"fundWithMppSession→intent="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:
| Field | Charge | Session | Meaning |
|---|---|---|---|
intent, method, challenge_id | ✓ | ✓ | Active Payment Auth challenge metadata |
settlement_asset, settlement_network | ✓ | ✓ | Denomination Harbor snapshots (USDC on Tempo for MVP) |
deposit_amount_base_units | ✓ | ✓ | USD cents × 10 000 for Tempo USDC base units |
session_protocol, channel_id | — | ✓ | Tempo v2 channel identity |
accepted_cumulative_base_units, pending_cumulative_base_units | — | ✓ | Voucher metering state |
descriptor_hash, channel_status | — | ✓ | Channel 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)
| Step | HTTP | Request headers | Response |
|---|---|---|---|
| 1. Challenge | POST /verify | Authorization: Bearer <api_key> + verify JSON body | 200 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. Credential | POST /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 voucher | POST /verify | Credential present but invalid or over budget | 200 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_unitsor setpending_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:
| Control | Behavior |
|---|---|
| Challenge TTL | Expires 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 digest | Challenges 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 binding | Credentials echo an opaque blob tying the challenge to tenant_id, intent_id, and (for verify) scope: verify_voucher. |
| Challenge ID consumption | Each 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.
| Rule | Why |
|---|---|
Never pass tenant_id from unauthenticated user input | Kit 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 payloads | Destinations are server-owned from settlement config. Client-supplied routing is ignored or rejected. |
| Bind recognition proofs to the authenticated tenant | verifier_context.tenant_id must match paybond.harbor.tenantId. |
| One capability token per intent | Tokens are minted when that intent reaches funded. Reusing across intents fails verify. |
| Treat tenant/intent mismatch errors as fatal | Do not retry HarborHttpError or plain Error messages about tenant or intent mismatch — investigate immediately. |
| Do not log credentials | Never 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 showsstripe_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_URLwhen live tenants allowstripe_mppsession funding - Hosted sandbox Harbor has
PAYBOND_STRIPE_MPP_TEST_SECRET_KEYwhen sandbox tenants allowstripe_mpp -
stripe_mppadded to allowed rails
Application developer
-
settlementRail: "stripe_mpp"/settlement_rail: "stripe_mpp"withcurrency: "usd" - Chose charge or session helper before first
/fund - App-owned
createPaymentCredential/create_payment_credentialwired - Fresh recognition proof per
/fundattempt - Guard tools only after
capability_tokenis 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
fundWithMppChargewhen you need streaming vouchers — switch tofundWithMppSessionat create time. - Sending
Authorization: Paymentthrough Gateway — usex-paybond-payment-authorizationso Bearer auth still works. - Treating
402+ problem+json as success — only200+capability_tokenmeans funded;202+session_open_pendingmeans 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.