@myzonerocks/pact
v0.1.9
Published
TypeScript SDK for the PACT payment abstraction protocol
Readme
@myzonerocks/pact
The TypeScript SDK for PACT, a payment abstraction protocol. PACT expresses a payment as a small sequence of typed, signed messages that any settlement provider can fulfil, carried over any transport. Every payment is a corridor — collect from the payer, convert the money, deliver to the recipient — so any pay-in provider composes with any pay-out provider across any currency, whether the money moves over a card network, a mobile-money wallet, or a blockchain.
This package is the browser- and Node-ready kernel plus the provider adapters. It owns the protocol messages, the state machine, the event-sourced ledger, the corridor router, and the leg, bridge, and compliance seams. It does not own transport, key custody, liquidity, the FX feed, or KYC policy — those are seams you inject, so the same code serves a wallet app and a custodial backend alike.
Install
bun add @myzonerocks/pact
# or: npm install @myzonerocks/pactThe kernel is exported from the package root; each adapter is a subpath so a bundle pulls in only the providers it uses.
import { Client, Money, encodePayload, decodePayload } from "@myzonerocks/pact";
import { StripeLeg } from "@myzonerocks/pact/adapters/stripe";
import { Erc20Leg } from "@myzonerocks/pact/adapters/erc20";Drive a corridor
intent.amount is what the recipient receives; the corridor grosses it up into
what the payer pays, with exact-integer math that always rounds in the recipient's
favour. Settlement is route-and-forget: initiate kicks off the pay-in and returns,
and a normalized provider event feeds advance, which is idempotent so a payment
settles once.
const client = new Client({ payIn: [stripe], payOut: [payout], verifier });
const intent = client.createIntent({
senderRef: payer,
recipientRef: merchantAccount,
amount: Money.parse("USD", 2, "1500"), // recipient receives $15.00
expiresAt: now + 600_000,
allowedRails: ["card"],
});
const quotes = await client.quoteOptions(intent, [{ payInAdapterId: "stripe", currency: "USD", exponent: 2 }]);
const quote = await client.select(quotes, payer);
const auth = await client.authorize(intent, quote, signer);
// The payer confirms the card on their own device, so create the intent awaiting
// their confirmation and hand the client secret to the card form.
const { preparation } = await client.interactiveInitiate(intent, quote, auth);
// ...payer confirms; the provider's webhook is normalized and fed to advance:
await client.advance(intent.id, "stripe", event); // -> settled, onceFor a server-side or saved-method charge, use initiate instead, which collects in
one step rather than waiting on the payer.
Carry a payment over your transport
The kernel emits and consumes opaque bytes; it never touches transport. Wrap a
message with encodePayload to get a self-identifying, versioned blob to place
inside your own envelope — a chat message, a queue entry — and decodePayload on
receipt. No plaintext ever needs to reach a relay.
const blob = encodePayload(intent); // place inside your E2EE envelope
const message = decodePayload(received); // recover on the other sideAdapters
An adapter translates between PACT messages and one provider, as a pay-in leg
(collect), a pay-out leg (disburse), or both. Be honest in payInCapabilities /
payOutCapabilities — the kernel refuses any operation you did not promise, so an
irreversible rail declares a counter-transfer refund rather than fake a reversal.
| Subpath | Provider | Legs |
|---|---|---|
| @myzonerocks/pact/adapters/stripe | Stripe | card pay-in (brings Apple Pay, Google Pay, Cash App Pay as methods) |
| @myzonerocks/pact/adapters/erc20 | ERC-20 | crypto pay-in + pay-out |
| @myzonerocks/pact/adapters/mpesa | M-Pesa | STK pay-in + B2C pay-out |
| @myzonerocks/pact/adapters/paypal | PayPal | Orders pay-in (Venmo) + Payouts pay-out |
Server-side adapters (Stripe, M-Pesa, PayPal) hold provider secrets and verify webhooks, so run them on a server, never in a browser bundle.
Design commitments
- Money is exact integer minor units at arbitrary precision, never a float — an 18-decimal token overflows 64 bits at nine whole tokens.
- Signing is over a canonical preimage, not JSON or protobuf bytes, with a domain-separation tag per message kind, so a signature verifies across every SDK and can never be replayed across message kinds, intents, or quotes.
- Every step is idempotent and replay-safe; a duplicated webhook settles once.
- The state machine is total: every illegal transition is rejected.
- Cross-provider settlement is never faked as atomic: value rests in escrow between the legs, so a failed pay-out unwinds to a refund rather than a loss.
Conformance
The Go, TypeScript, Dart, Swift, and Kotlin SDKs are held to one wire format by shared vectors: a fixed intent produces a fixed canonical preimage, hash, and signature that every one of them reproduces byte-for-byte.
bun run testLearn more
The protocol is specified in the repository at
github.com/myzonerocks/pact — spec/PACT.md
for messages, canonical signing, the ledger, and conformance, and
spec/cross-rail.md for the corridor model.
Licensed under Apache-2.0.
