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
- The client sends the signed
fundcall without proof. - The server returns
402with an amount, escrow destination, andrailData. - Mock and the current direct-transfer x402 adapter retry with proof in
X-PAYMENT. Native MPP instead returnsWWW-Authenticate: Payment, verifies the retry’sAuthorization: Payment, and returnsPayment-Receiptafter recording the collection. - The adapter verifies and collects the payment, then the ledger records the deposit.
- The funding request activates the pact only when every required party,
minParties, and every declared open slot are satisfied. Otherwise, thefundBytransition 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:
collectverifies a real ERC-20 transfer receipt, payer address binding, and exact amount.releaseandrefundtransfer from the operator-held escrow key.- Party-to-EVM address binding is signed and first-write-wins.
settlementIdprevents 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 USDCThe 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
/healthresponse immediately before use. MPP is available only when it returns HTTP 200 withok: true,railslistsmpp,paymentRails.mpp.liveandpaymentRails.mpp.solvency.okaretrue, andpayoutReadiness.treasury.mppistrue. 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 buyerAccount 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:
- Create a dedicated Tempo escrow signer. Store
MPP_ESCROW_PKin the deployment secret manager; never put it in source, a client, chat, an MCP call, or a log. - Generate a dedicated
MPP_SECRET_KEYwith 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 stableMPP_REALMand canonical HTTPSPACT_PUBLIC_URL; the server pins the secret-derived challenge identity, realm, public origin, and escrow address against durable payment state. - Set
MPP_RPC_URLandMPP_RECONCILIATION_RPC_URLto reviewed Tempo mainnet RPC endpoints on different HTTPS hostnames. The currently verified bootstrap pair uses Tempo’s officialhttps://rpc.tempo.xyzas the transaction-preparation and broadcast primary, withhttps://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 ID4217through 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 . - Accept only mainnet USDC.e at
0x20c000000000000000000000b9537d11c60e8b50, which is fixed by the server rather than configured by anMPP_USDCoverride. Make themppxtempo.chargerecipient the escrow signer’s address. - Require each party to sign a first-write-wins Tempo
railAddressbinding. Verify that binding before collection and before any release or refund; never treat a Pact Ed25519 PartyId as a Tempo payout address. - 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.
- Launch fail-closed. Live startup requires
PACT_ENV=canaryorPACT_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. Keepmppout of/healthunless the primary and independent reconciliation RPCs, escrow signer/address, USDC.e asset, treasury payout address, fee reserve, solvency, and outbound payout path are ready. - 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.
- Run the smallest permitted money-moving mainnet canaries before public activation: exactly
10000atomic units (0.01USDC.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 budgetThe 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:
- collect with exact party and amount attribution
- release and refund to the correct recipient
- idempotent retry with the same
settlementId - concurrent reuse rejection for the same x402 transaction or MPP credential/receipt
- 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.