Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/calm-rivers-charge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@inflowpayai/mpp-seller': minor
---

Add an async Stripe charge method that uses the official mppx wire schema, loads the authenticated seller profile from
InFlow, validates exact USD limits before challenge issuance, binds credential references to seller-provided references,
and delegates validation and settlement to the PSP. Document the Stripe Connect setup required for sellers.
14 changes: 8 additions & 6 deletions docs/mpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ for x402's.
| `@inflowpayai/mpp-seller` | `Method.toServer` + InFlow redeem/settle driver | Accepting MPP payments as a seller. |
| `@inflowpayai/mpp-buyer` | `Method.toClient` + InFlow buyer-endpoint driver | Paying via MPP. |

All packages publish under the `@inflowpayai` scope and declare [`mppx`](https://github.com/wevm/mppx)`@^0.6.28` as a
All packages publish under the `@inflowpayai` scope and declare [`mppx`](https://github.com/wevm/mppx)`@^0.8.17` as a
peer. The seller/buyer packages additionally re-export `Mppx` from the appropriate `mppx` entry (`mppx/server` /
`mppx/client`) so consumers get a single import.

Expand Down Expand Up @@ -72,11 +72,13 @@ const tx = await mpp.createTransaction({ challenge });

The seller package (`@inflowpayai/mpp-seller`) attaches non-mutating validation and authoritative broadcast behavior to
`Method.toServer`: an unpaid request returns a locally issued `402` challenge, and a paid one is validated and settled
through InFlow. It exports seller methods for `inflow` and `tempo`. To accept **multiple InFlow currencies** on one
route (one challenge per currency), use the package's `inflowCharges` / `inflowChargesNodeListener` helpers over the
core `mppx/server` instance — the framework adapters expose only the single-currency `charge`. See
[architecture.md](./architecture.md) for the PSP boundary, and
[`examples/mpp-seller-express`](../../examples/mpp-seller-express) or
through InFlow. It exports seller methods for `inflow`, `tempo`, and one-time Stripe charges. `await stripe(...)` reads
the authenticated seller's Stripe profile capability from InFlow. Sellers link their Stripe account through Stripe
Connect in the InFlow dashboard; the SDK uses an InFlow API key, and InFlow handles Stripe settlement with its platform
credentials and the seller's connected-account ID. To accept **multiple InFlow currencies** on one route (one challenge
per currency), use the package's `inflowCharges` / `inflowChargesNodeListener` helpers over the core `mppx/server`
instance — the framework adapters expose only the single-currency `charge`. See [architecture.md](./architecture.md) for
the PSP boundary, and [`examples/mpp-seller-express`](../../examples/mpp-seller-express) or
[`examples/mpp-seller-hono`](../../examples/mpp-seller-hono) for the complete runnable shape.

## Quickstart — buyer
Expand Down
6 changes: 5 additions & 1 deletion docs/mpp/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ During the credential lifecycle it correlates by the server-stamped `transaction
problem). `mppx` composes these into its compatibility `verify` hook. This is the direct analog of x402-seller
delegating validation/settlement to the InFlow facilitator. For the `inflow` method, a single charge advertises one
currency; to offer several, the seller emits one challenge per currency via `compose(...)` — surfaced by the package's
`inflowCharges` helper, the MPP analog of x402-seller's `inflowAccepts`.
`inflowCharges` helper, the MPP analog of x402-seller's `inflowAccepts`. The Stripe seller method follows the same
validate/broadcast boundary but reuses mppx's official `stripe/charge` schema. It initializes asynchronously so the
required business-profile id and payment types come from the authenticated InFlow config, never from route input.
InFlow uses its platform Stripe credentials with the seller's connected-account ID and owns PaymentIntent replay,
recovery, and settlement. The seller application supplies only an InFlow API key.
- The **buyer** package's `Method.toClient.createCredential` methods do not sign locally. They forward the parsed
challenge to `POST /v1/transactions/mpp`, poll `GET /v1/transactions/{id}/mpp` through the `pending → ready`
lifecycle, and return the server-produced credential, re-serialised for the `Authorization: Payment` header.
Expand Down
57 changes: 54 additions & 3 deletions packages/mpp-seller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ authoritative replay or authorization guard.
- `tempo(parameters)` — the seller `tempo` method for Tempo TIP-20 charges. Pass it to
`Mppx.create({ methods: [tempo({ apiKey, currency, recipient })], secretKey })`. Fee-payer sponsorship defaults to
off; set `methodDetails.feePayer: true` (on the method or per charge) to mint a sponsored challenge.
- `await stripe(parameters)` — the seller `stripe/charge` method for one-time USD payments with Stripe Shared Payment
Tokens. It loads the authenticated seller's verified Stripe business-profile capability from InFlow before returning a
method. Validation and settlement still use InFlow's `/validate` and `/broadcast` endpoints; no Stripe secret enters
the application or SDK.
- `inflowCharges(mppx, prices)` — present several currencies on one route. Returns the Web-fetch handler from
`compose(...)`: one `WWW-Authenticate` challenge per price (the MPP analog of `@inflowpayai/x402-seller`'s
`inflowAccepts`). See [Multiple currencies](#multiple-currencies) below.
Expand All @@ -53,10 +57,13 @@ authoritative replay or authorization guard.
- `Mppx` and `Expires` (re-exported from `mppx/server`) and `Receipt` (from `mppx`) — a single import gives the
foundation server handler and the InFlow methods.
- `Discovery` (from `mppx/discovery`) — generates and parses OpenAPI `x-payment-info.offers[]` metadata.
- Types: `InflowSellerParameters`, `TempoSellerParameters`, `LoadedConfig`, `InflowChargePrice`, plus the core
re-exports `Environment`, `MppCurrencyRail`, `MppProblemDetail`, `MppReceipt`.
- Types: `InflowSellerParameters`, `StripeSellerParameters`, `TempoSellerParameters`, `LoadedConfig`,
`InflowChargePrice`, plus the core re-exports `Environment`, `MppCurrencyRail`, `MppProblemDetail`, `MppReceipt`.
- Errors: `MppUnsupportedCurrencyError` (charge currency has no rail in the PSP config), `MppCredentialProblemError`
(credential validation or broadcast failed; carries the PSP's RFC 9457 problem).
(credential validation or broadcast failed; carries the PSP's RFC 9457 problem), `MppStripeUnavailableError` (seller
has no safe Stripe capability), and `MppStripeAmountError` (amount is below $0.50, above $999,999.99, or cannot be
expressed as exact cents). `MppStripeRequestError` identifies unsupported metadata or an `externalId` longer than 255
characters. Malformed request fields can raise the foundation schema's validation error before these SDK checks.

## Configuration

Expand Down Expand Up @@ -123,6 +130,50 @@ This package ships no middleware of its own; use `mppx`'s framework adapters (`m
[`examples/mpp-seller-express`](../../examples/mpp-seller-express) and
[`examples/mpp-seller-hono`](../../examples/mpp-seller-hono) for the complete runnable shape.

## Stripe one-time charges

Connect your Stripe account through the InFlow dashboard and ensure the account has an eligible Stripe business profile
before enabling Stripe payments. Your application needs an InFlow Seller API key, not a Stripe secret key. InFlow uses
its platform credentials and your connected-account ID to process payments on your Stripe account.

Stripe challenges use the official `mppx@^0.8.17` `stripe/charge` schema. The seller SDK reads your Stripe
business-profile ID (`networkId`) and allowed payment methods from `GET /v1/mpp/config`. For that reason, `stripe(...)`
is asynchronous and fails at initialization if the required Stripe profile is unavailable.

```ts
import { Mppx, stripe } from '@inflowpayai/mpp-seller';

const stripeMethod = await stripe({
apiKey: process.env.INFLOW_API_KEY!,
environment: 'sandbox',
});

const mppx = Mppx.create({
methods: [stripeMethod],
secretKey: process.env.MPP_SECRET_KEY,
});

export async function handler(request: Request) {
const result = await mppx.charge({ amount: '1.00', externalId: 'order-123' })(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json({ access: 'granted' }));
}
```

The method accepts USD amounts from `0.50` through `999999.99`, with no more than two fractional digits. It rejects
values such as `0.49` or `0.501` before issuing a challenge instead of rounding them. The SDK always replaces any
caller-supplied profile id, currency, decimals, or payment-method list with the authenticated server configuration. Only
one-time Stripe charges are supported; this method does not advertise subscriptions or InFlow buyer initiation.

If you include an `externalId` in the challenge, the buyer's credential must echo it exactly. Credentials with a missing
or different reference are rejected before payment. When the challenge has no `externalId`, a buyer reference is
optional.

For composed offers, `canOffer` receives the same authoritative request as the issued challenge, with the amount in
integer cents. Metadata allows up to 45 string entries, with keys up to 40 characters and values up to 500 characters;
keys cannot be blank, contain square brackets, or use `externalId`, `inflowMppTransactionId`, `mppChallengeId`,
`mppIntent`, `mppMethod`, or `stripeNetworkProfile`.

## Multiple currencies

`charge(...)` advertises **one** currency per route. Per the MPP core spec, multiple currencies are multiple challenges
Expand Down
2 changes: 1 addition & 1 deletion packages/mpp-seller/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@inflowpayai/mpp-seller",
"version": "0.8.3",
"description": "InFlow MPP SDK: seller-side `inflow` method built as a native mppx server method — challenges are minted and HMAC-bound locally by mppx, validation is non-mutating, and broadcast delegates settlement to the InFlow PSP.",
"description": "InFlow MPP seller SDK: local mppx challenges with non-mutating validation and authoritative InFlow settlement for InFlow, Tempo, and Stripe payments.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
Expand Down
31 changes: 31 additions & 0 deletions packages/mpp-seller/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,34 @@ export class MppUnsupportedRailError extends Error {
super(`inflow: rail "${rail}" is not supported for currency "${currency}" and intent "${intent}"`);
}
}

/** Thrown when the authenticated seller cannot safely advertise Stripe through the PSP config. */
export class MppStripeUnavailableError extends Error {
override readonly name = 'MppStripeUnavailableError';

constructor() {
super(
'stripe: this seller has no verified Stripe business profile; connect one in InFlow before offering Stripe payments',
);
}
}

/** Thrown before challenge issuance when a Stripe USD amount cannot be represented or accepted exactly. */
export class MppStripeAmountError extends Error {
override readonly name = 'MppStripeAmountError';

/** @param reason - Actionable constraint violated by the amount. */
constructor(reason: string) {
super(`stripe: ${reason}`);
}
}

/** Thrown before challenge issuance when Stripe request fields would be rejected by the buyer or PSP. */
export class MppStripeRequestError extends Error {
override readonly name = 'MppStripeRequestError';

/** @param reason - Actionable request constraint violated by the seller. */
constructor(reason: string) {
super(`stripe: ${reason}`);
}
}
6 changes: 5 additions & 1 deletion packages/mpp-seller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// no `./server` subpath; the foundation `Mppx` server handler is re-exported here so a single import gives both the
// handler and the InFlow method: `import { Mppx, inflow } from '@inflowpayai/mpp-seller'`.

export { inflow, tempo } from './methods.server.js';
export { inflow, stripe, tempo } from './methods.server.js';
export type { StripeSellerParameters } from './methods.server.js';

export {
inflowCharges,
Expand All @@ -19,6 +20,9 @@ export {
MppAmbiguousRailError,
MppInstrumentRequiredError,
MppCredentialProblemError,
MppStripeAmountError,
MppStripeRequestError,
MppStripeUnavailableError,
MppUnsupportedCurrencyError,
MppUnsupportedRailError,
} from './errors.js';
Expand Down
Loading
Loading