Skip to Content
Payment rails

Payment rails

Pact exposes one funding lifecycle and one escrow ledger across the rail adapters enabled by a deployment. A pact selects one rail and one asset at creation; cross-rail settlement is intentionally outside v1. The selected server’s /health response is the source of truth for enabled rails. Use MPP only when it returns HTTP 200 with ok: true, lists mpp, and reports paymentRails.mpp.live: true, paymentRails.mpp.solvency.ok: true, and payoutReadiness.treasury.mpp: true.

interface Receipt { rail: string; party: PartyId; amount: string; asset: string; ref: string; includedAt?: number; responseHeaders?: Record<string, string>; attemptId?: string; } interface PaymentChallenge { responseHeaders: Record<string, string>; railData: unknown; } interface FundingContext { pactId: string; potAmount: string; bondAmount: string; reservationId?: string; railAddress?: string; preparedAttemptId?: string; requestScope?: string; challengeContext?: string; challengeIdentity?: string; challengedAt?: number; credentialDeadline?: number; fundDeadline?: number; queryOnlyClaim?: boolean; } type CollectionPreparation = | { kind: "prepared"; attemptId: string } | { kind: "claim_overflow" }; interface RailAdapter { readonly name: string; readonly supportedAssets?: readonly string[]; normalizeAddress?(address: string): string; info?(): Record<string, unknown>; solvency?(): Promise<Record<string, unknown>>; escrowAccount(ledgerId: string): string; validateAddress?(address: string): string; requirement(ledgerId: string, party: PartyId, amount: Money): Promise<unknown> | unknown; challenge?( ledgerId: string, party: PartyId, amount: Money, request: Request, ): Promise<PaymentChallenge>; prepareCollection?( ledgerId: string, from: PartyId, amount: Money, proof: unknown, funding: FundingContext, ): CollectionPreparation; collect( ledgerId: string, from: PartyId, amount: Money, proof: unknown, funding?: FundingContext | string, ): Promise<Receipt>; reconcileCollections?(): Promise<number>; reconcilePayouts?(): Promise<number>; release(ledgerId: string, to: PartyId, amount: Money, settlementId: string): Promise<string>; refund(ledgerId: string, to: PartyId, amount: Money, settlementId: string): Promise<string>; }

Every adapter must preserve party attribution, exact amounts, idempotent settlement, and the ledger invariant that completed payouts never exceed deposits.

Shared 402 funding flow

  1. The client sends the signed fund call without proof.
  2. The server returns 402 with an amount, escrow destination, and railData.
  3. Mock and the current direct-transfer x402 adapter retry with proof in X-PAYMENT. Native MPP instead returns WWW-Authenticate: Payment, verifies the retry’s Authorization: Payment, and returns Payment-Receipt after recording the collection.
  4. The adapter verifies and collects the payment, then the ledger records the deposit.
  5. The funding request activates the pact only when every required party, minParties, and every declared open slot are satisfied. Otherwise, the fundBy transition may activate it after dropping optional parties and slots, or cancel and refund it when requirements are unmet.

This is an HTTP contract, not an assumption that every rail is blockchain-based. Keep every real payment proof or credential out of chat, MCP calls, argv, environment variables, logs, and files. The current direct-transfer x402 path is pact fund <pactId> --rail-address <address> --proof-stdin, or an equivalent in-memory secret input in a custom client. Native MPP instead uses the named-account, human-approved flow below; it does not accept an x402 proof through stdin.

Mock

The database-backed mock rail is always enabled. Use it for unit tests, local E2E work, simulations, and SDK development. It supports deterministic balances and failure injection through the test hooks while preserving escrow state across a server restart.

Mock proofs use {"scheme":"mock"}. They have no value outside the development server.

x402

The v1 x402 adapter uses USDC on Base Sepolia and a custodial escrow wallet. It rejects a pact that selects any other asset:

  • collect verifies a real ERC-20 transfer receipt, payer address binding, and exact amount.
  • release and refund transfer from the operator-held escrow key.
  • Party-to-EVM address binding is signed and first-write-wins.
  • settlementId prevents a repeated payout intent from paying twice.

Production environment:

X402_RPC_URL= X402_ESCROW_PK= PACT_TREASURY_X402_ADDRESS=0x... X402_USDC= # optional; defaults to Base Sepolia USDC

The treasury address is the payout destination for forfeited bonds. When x402 is enabled, startup fails without it and /health.payoutReadiness.treasury.x402 must be true before funding.

For real conformance tests, the payer side also needs X402_PAYER_PK and testnet balances. Pact v1 verifies a direct ERC-20 transfer receipt. It does not use a facilitator-based authorization flow.

Warning: v1 x402 escrow is custodial. The service operator controls the escrow private key. An on-chain hold contract is a future protocol boundary, not a property of v1.

MPP

Availability: Check the selected server’s /health response immediately before use. MPP is available only when it returns HTTP 200 with ok: true, rails lists mpp, paymentRails.mpp.live and paymentRails.mpp.solvency.ok are true, and payoutReadiness.treasury.mpp is true. Otherwise, do not send an MPP credential or infer support from this documentation.

Pact’s production architecture is a direct, self-hosted mppx integration using tempo.charge. There is no hosted MPP gateway to sign up for, and MPP or Tempo does not issue an MPP_BASE_URL or MPP_API_KEY for this design. The payment handler runs inside the Pact server: it creates WWW-Authenticate: Payment challenges, verifies Authorization: Payment credentials, and returns Payment-Receipt, with funds collected into an operator-controlled Tempo escrow account. Pact then sends release and refund transfers from that escrow account.

For a human-approved client payment, Pact CLI v0.3.3 uses a named mppx account in the operating-system keychain and requires an explicit USD cap. This is a staged procedure, not one script to run end to end. First read the selected server and exact pact:

curl -fsS https://api.pact.sh/health pact get <pactId>

Stop before wallet creation if either read fails, MPP readiness is false, or any of the pact’s five evaluator fields differs from /health. Derive this party’s exact principal from its deposit plus bond. Only after the human explicitly approves that amount and a separate fee reserve of at most 0.01 USDC.e, create or inspect the named account:

pact wallet mppx create --account buyer pact wallet mppx view --account buyer

Account creation is local-only: it writes to the OS keychain, makes no network request, invokes no faucet, and funds nothing. Its JSON output identifies Tempo mainnet, USDC.e, the address, minimum funding amount, and a next-command template containing <pactId>. Fund that returned address with exactly the separately approved principal and fee reserve through a separately approved wallet or exchange action. Immediately before payment, read health and the pact again:

curl -fsS https://api.pact.sh/health pact get <pactId>

Stop on any change. Obtain fresh approval, then run only the exact capped command:

pact fund <pactId> --payer mppx --account buyer --max-amount <approved-principal-cap-USD>

--max-amount caps only the Pact principal. The CLI separately rejects a transaction whose worst-case Tempo network fee exceeds 0.01 USDC.e.

Never set MPPX_PRIVATE_KEY or X402_PRIVATE_KEY. If the CLI reports an uncertain payment, do not create another credential or retry; reconcile the original transaction and funding attempt. Wallet creation and each payment require separate, explicit human confirmation.

Operator activation checklist

This is server-operator work, not a client installation step. Keep MPP absent from /health until every item is complete:

  1. Create a dedicated Tempo escrow signer. Store MPP_ESCROW_PK in the deployment secret manager; never put it in source, a client, chat, an MCP call, or a log.
  2. Generate a dedicated MPP_SECRET_KEY with at least 32 random bytes and store it in the same secret manager. This key signs and authenticates MPP challenges only; do not reuse it for another rail or service. It is not an API key obtained from a provider. Set a stable MPP_REALM and canonical HTTPS PACT_PUBLIC_URL; the server pins the secret-derived challenge identity, realm, public origin, and escrow address against durable payment state.
  3. Set MPP_RPC_URL and MPP_RECONCILIATION_RPC_URL to reviewed Tempo mainnet RPC endpoints on different HTTPS hostnames. The currently verified bootstrap pair uses Tempo’s official https://rpc.tempo.xyz as the transaction-preparation and broadcast primary, with https://rpc-tempo.t.conduit.xyz/ as the independent read/finality reconciliation endpoint. The public Conduit endpoint has no availability SLA and rejected an actual charge broadcast, so do not use it as the primary. Replace the primary with a managed endpoint such as Alchemy Tempo Mainnet  only after it passes the exact prepare/sign, nonbroadcast payout, and minimum-value charge canaries; after that switch, Tempo’s official endpoint can return to the reconciliation role. Verify chain ID 4217 through both before startup. Do not paste a provider URL containing an API key into chat, source, or a plaintext environment file. Tempo publishes network connection details  and developer infrastructure options .
  4. Accept only mainnet USDC.e at 0x20c000000000000000000000b9537d11c60e8b50, which is fixed by the server rather than configured by an MPP_USDC override. Make the mppx tempo.charge recipient the escrow signer’s address.
  5. Require each party to sign a first-write-wins Tempo railAddress binding. Verify that binding before collection and before any release or refund; never treat a Pact Ed25519 PartyId as a Tempo payout address.
  6. Implement exact-asset, exact-amount collection plus idempotent direct TIP-20 release and refund transfers. Persist the MPP receipt or transaction reference against the Pact party, ledger, amount, and funding attempt before crediting the deposit.
  7. Launch fail-closed. Live startup requires PACT_ENV=canary or PACT_ENV=production, invite access, durable evaluator and treasury identities, MPP_TREASURY_ADDRESS, real clocks with test hooks off, explicit funding/liability ceilings, and a daily payout-fee budget. Keep mpp out of /health unless the primary and independent reconciliation RPCs, escrow signer/address, USDC.e asset, treasury payout address, fee reserve, solvency, and outbound payout path are ready.
  8. Keep payouts fail-closed. Release and refund are direct Tempo transactions signed by the escrow wallet with explicit USDC.e fee caps. An ambiguous or unconfirmed transfer stays pending for exact-transaction reconciliation; never replace it with a fresh payout. Production/canary disables local-key and remote-relay sponsorship, so fund the escrow with a separate USDC.e fee reserve and use payer mode for incoming charges.
  9. Run the smallest permitted money-moving mainnet canaries before public activation: exactly 10000 atomic units (0.01 USDC.e) for one release case and one refund case. That is the implementation-enforced minimum; a one-minor-unit canary is rejected. Prove exact collection, recipient arrival, idempotent retry, restart recovery, and final ledger reconciliation while keeping the separately bounded payout-fee reserve funded.

Expected operator secrets and configuration:

PACT_ENV=canary # use the loss-capped profile for initial activation PACT_ACCESS_MODE=invite MPP_RPC_URL=https://rpc.tempo.xyz MPP_RECONCILIATION_RPC_URL=https://rpc-tempo.t.conduit.xyz/ # temporary public read/finality peer MPP_ESCROW_PK= # dedicated server-side Tempo escrow signer MPP_SECRET_KEY= # 32+ random bytes; MPP challenge signing only MPP_REALM="Pact MPP" # keep stable after the first live challenge PACT_PUBLIC_URL=https://api.pact.sh MPP_TREASURY_ADDRESS=0x... MPP_MAX_FUNDING_AMOUNT_ATOMIC=10000 MPP_MAX_ESCROW_LIABILITY_ATOMIC= # required positive atomic-unit ceiling MPP_PAYOUT_DAILY_FEE_BUDGET_ATOMIC= # required positive atomic-unit budget

The canary profile caps one funding action at 10000 atomic units (0.01 USDC.e). Set the liability and 24-hour payout-fee budgets to the smallest positive values that cover the approved canaries and at least one configured payout-fee cap; the current canary hard ceilings are 100000 atomic units (0.10 USDC.e) for each. Choose a genuinely independent reconciliation provider and review the stricter per-payout fee caps before injecting any key.

The official MPP server quickstart , tempo.charge guide, and Tempo documentation  define the external protocol and network contracts. A local fake adapter can test Pact’s internal RailAdapter behavior, but it is not MPP wire conformance and is never evidence that production MPP is available.

Verify a deployed rail

Start with the deployed runtime, not a client repository or a local server-source command:

export PACT_SERVER=https://api.pact.sh curl -fsS "$PACT_SERVER/health"

/health lists only the rails enabled in that deployment. Presence in /health proves configuration, not end-to-end settlement. Release evidence for each enabled rail must cover:

  1. collect with exact party and amount attribution
  2. release and refund to the correct recipient
  3. idempotent retry with the same settlementId
  4. concurrent reuse rejection for the same x402 transaction or MPP credential/receipt
  5. recovery after interruption between external payment and local completion

For MPP, the runtime readiness check is stricter than rail presence: require HTTP 200, ok: true, paymentRails.mpp.live: true, paymentRails.mpp.solvency.ok: true, and payoutReadiness.treasury.mpp: true before a canary or user funding action.

Credential-free tests may use a local Anvil chain or a fake MPP adapter, but those results must be labelled simulated. A fake adapter does not exercise the standard MPP wire exchange or Tempo mainnet settlement and does not establish production availability.

Real-environment verification

Real-environment verification belongs on a dedicated operator test deployment with funded test identities. Exercise the rail’s exact wire flow through create → pay → settle → payout, then preserve the external transaction or MPP receipt identifiers as release evidence. Never infer real-rail success from mock results or run money-moving QA against public production without operator authorization.

Use Test and verify for the public rail acceptance flow and the checks required before enabling real funds.