In short: this is the funding path for one specific rail (Stripe MPP), so an agent's usage-metered spend can move machine-to-machine over HTTP. 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.
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). Paybond verifies the credential, confirms the charge, then mints capability_token.
Session opens a Tempo payment channel: one Paybond intent maps to one channel. Paybond verifies the open-session credential, confirms the on-chain reserve deposit (Tempo testnet in sandbox, mainnet in production), 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.
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 and voucher metering | Session (requires Tempo recipient configured) |
For protocol background and status mapping, see the stripe_mpp support matrix row and MPP Payment Auth handshake.
Payment Auth header flow
Paybond 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 with 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 with Payment-Receipt: …; Cache-Control: private or 202 (session pending) or 402 with application/problem+json | state = funded, capability_token on success |
Gateway vs Harbor path
| 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 | Authorization: Payment … | Standard Payment Auth scheme on the upstream request |
Gateway translates x-paybond-payment-authorization → Authorization: Payment when forwarding. Response headers (WWW-Authenticate, Payment-Receipt, Cache-Control, x402-compat payment-required / payment-response) pass through unchanged.
Invalid credentials
When a credential is rejected, 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 a 402 with a problem body as funded. Reuse the same idempotency-key and empty JSON body {} on safe retries.
Session deposit pending (202 Accepted)
Tempo session funding may return 202 Accepted when the open transaction is broadcast but the on-chain reserve deposit is not confirmed yet:
stateremainsopenfunding.status=session_open_pendingfunding.channel_idandlast_settle_tx_hashare available for support debugging- Kit helpers poll
/funduntilcapability_tokenis present or a terminal failure surfaces (PaybondMppFundingPendingErrorwhen polling exhausts)
Paybond also reconciles pending session opens in the background and mints 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 the open credential was accepted.
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
In sandbox, MPP funding runs against Stripe test mode — the same Payment Auth flow as production, without live money. Before you fund, paste your Stripe test MPP profile id (profile_test_...) in Configuration → Settlement and enable the stripe_mpp rail.
| Behavior | Sandbox | Production |
|---|---|---|
| Stripe charge path | Stripe test mode with your profile_test_* profile | Stripe live mode with your profile_* profile |
| Tempo deposit confirmation | On-chain on Tempo testnet (Moderato) when session funding is available; otherwise session open may stay session_open_pending | On-chain on Tempo mainnet after the deposit confirms |
| MPP profile & Tempo recipient | Set in Settlement settings (profile_test_*, optional Tempo recipient); masked in console | Set in Settlement settings; masked in console |
First /fund without credential | May return dual WWW-Authenticate headers (charge and session) only when Tempo session funding is enabled and a recipient address is set | 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 Tempo session funding is enabled and a 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"
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; end-to-end Stripe test-mode MPP and Tempo testnet checks are opt-in.
Kit integration
One-shot charge (TypeScript)
paybond-session.ts
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)
paybond_session.py
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_tokenPrefer 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> and 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 and 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
Paybond 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.
- Vouchers whose cumulative total exceeds the intent budget are rejected, 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 Tempo spend under the same intent ceiling.
Challenge binding on fund and verify
MPP challenges include:
| Control | Behavior |
|---|---|
| Challenge TTL | Expires at the earlier of intent deadline and a capped window (default 15 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 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. Mismatched tenant context is rejected 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 your authenticated workspace. |
| 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 errors 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. |
Cross-tenant fund or voucher attempts are rejected. Stale or replayed challenge_id and voucher credentials fail without changing intent state. Vouchers that would exceed the intent amount_cents budget are rejected.
Integration checklist
Tenant admin (before first MPP fund)
- Paid plan activated (production) or sandbox workspace selected
- Stripe Connect destination linked (MPP routes through your linked Stripe destination)
- In Configuration → Settlement, paste your MPP profile id (
profile_test_...in sandbox,profile_...in production) and confirm readiness shows the profile as configured - For session mode: set a Tempo recipient and enable Tempo session funding in Settlement settings
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
402withapplication/problem+jsonas success — only200with acapability_tokenmeans funded;202withsession_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 — the completion rule evaluates vendor tool results, not MPP challenge ids.
- EUR or multi-currency intents on
stripe_mpp— rejected at create until policy expands.