@atumlabs/mppx-atum-escrow
v0.4.1
Published
Atum cross-chain escrow payment method for the Machine Payments Protocol (MPP).
Readme
@atumlabs/mppx-atum-escrow
Copyright 2026 Atum Labs, Inc.
Licensed under the Apache License, Version 2.0. See the
LICENSEfile distributed with this package.
An mppx payment method that lets a merchant accept
cross-chain stablecoin payments through Atum, over the Machine Payments Protocol (MPP).
Full documentation: docs.atum.xyz.
- The payer funds the payment in any supported source asset and chain (e.g. USDC on Base, USDT on Tron, or USDC on Solana).
- The merchant receives an exact, pinned amount of the asset it chose, on the chain it chose (e.g. USDC on Arbitrum).
- Atum's escrow, auction, and settlement network bridges the two. Neither side has to hold or move the other's asset.
If you have used MPP with a card or same-chain method before, this is the same integration shape — you register a method and MPP does the 402 dance for you — except the money can cross chains.
What is MPP, in one paragraph
MPP is an "HTTP 402" protocol: a client requests a resource, the server answers 402 Payment
Required with a machine-readable description of how to pay, the client pays and retries, and the
server returns 200 plus a receipt. Unlike some 402 protocols, MPP has no central "facilitator"
service — the logic that validates and settles a payment runs inside the merchant's own
process, as a payment method plugged into the mppx SDK. This package is that method for
Atum. Its one verify() hook both checks the payment and settles it through the Atum Payment
Gateway.
How a payment flows
Payer (client) Merchant (server) Atum Payment Gateway
| | |
| ─ GET /paid-resource ────> | |
| <─ 402 + atum-escrow ───── | (what to pay, where it lands) |
| challenge | |
| (registerClient builds & | |
| signs the deposit auth) | |
| ─ retry + credential ────> | |
| | (registerServer.verify: |
| | checks signature & terms) |
| | ─ submit payment request ────> |
| | <─ FulfillmentConfirmation ─── |
| <─ 200 + resource + ────── | |
| receipt | |- The merchant answers a request with a
402challenge describing what it receives (destination asset, chain, exact amount) and the source option a payer may fund from. You build this challenge from a corridor withbuildChargeChallenge. - The payer's
registerClientreads the challenge, builds an Atum payment request, signs the source-chain deposit authorization for that chain, and returns it as an MPP credential. - The merchant's
registerSerververifies the credential (recovers the signature, matches every term against the challenge) and submits it to the Atum Payment Gateway. Once the payment settles it returns a receipt carrying the settlement confirmation. A payment that settles more slowly than the gateway's synchronous window reports as pending, and the payer's retry of the same purchase collects the result — see Settlement outcomes.
The source-chain authorization is built and signed through the Atum
@atumlabs/payment-gateway-client,
so one code path supports EVM, Tron, and Solana sources — you configure a source
and the method picks the right signing scheme for its chain.
Install
npm install @atumlabs/mppx-atum-escrowThis package is an mppx plugin, so mppx and zod are peer dependencies — it registers into
your mppx instance and must share the same mppx and zod as the rest of your integration.
Install them if your project doesn't already depend on them:
npm install mppx zodThe merchant examples below also use
@atumlabs/payment-gateway-client
for the gateway connection and corridor defaults.
Key concepts
| Term | What it means |
| --- | --- |
| Corridor | The merchant's payment configuration: what it receives (destination), and which source chains/tokens it accepts. You define it once. |
| Source option | A source chain (with its escrow and role addresses) and one or more tokens (assets) a payer may pay from on it. A corridor may list several; each challenge offers one (chain, token). |
| fulfillmentAmount | The exact amount, in the destination token's atomic units, the merchant will receive. USDC/USDT use 6 decimals, so "10000000" = 10 USDC. Set per charge. |
| Source cap | The most the payer can spend on the source side: fulfillmentAmount + a markup (markupBps) to cover the cross-chain spread. The payer signs this cap. |
| Escrow | The source-chain contract the payer's funds lock into until the merchant is paid. |
| Deadlines | Two budgets in seconds: quoteDeadlineSeconds (how long the auction runs) and fulfillmentDeadlineSeconds (how long settlement may take). Required order: now < quote < fulfillment. |
| PaymentSubmitter | A small adapter you provide that hands a signed payment request to the Atum Payment Gateway and returns the result. |
| intentId | Identifies the purchase (your order or invoice id): one value per purchase, reused on every retry of it. The payment's identity derives from it, so it is what makes a retry safe. |
| Receipt | What verify() returns on success: the MPP receipt plus the full settlement confirmation. |
All addresses and amounts are strings; all amounts are atomic units (never floats). Addresses
are in each chain's native form — 0x… hex for EVM, base58 for Tron and Solana.
Which chains and assets can a corridor use?
Atum supports many corridors. The identifier for every supported token is listed under
supported assets, and the chain ids
under supported networks. Assets
are named with CAIP-19 identifiers, for example
eip155:84532/erc20:0x036CbD53842c5426634e7929541eC2318f3dCF7e for USDC on Base Sepolia.
Copy identifiers exactly: base58 values, such as Solana token mints and account addresses, are
case-sensitive. The escrow and role addresses for each chain are filled in by
corridorFromDefaults, so you never paste those by hand.
Quick start — merchant (server)
import { Mppx } from "mppx/server";
import { PaymentGatewayClient } from "@atumlabs/payment-gateway-client";
import {
registerServer,
buildChargeChallenge,
corridorFromDefaults,
type AtumEscrowCorridor,
type PaymentSubmitter,
} from "@atumlabs/mppx-atum-escrow/server";
const gateway = new PaymentGatewayClient({ BASE: "https://gateway.example.com" });
// 1. Describe your corridor: you receive 10 USDC on Arbitrum, and accept USDC on Base and
// USDT on Tron as sources. `corridorFromDefaults` fills the escrow/role/proxy addresses
// from the gateway, so you only specify what you receive, the sources, and your budgets.
const corridor: AtumEscrowCorridor = await corridorFromDefaults(gateway, {
destination: {
network: "eip155:42161", // Arbitrum One (CAIP-2)
asset: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
account: "0xYourMerchantReceiveAddress",
},
sources: [
// One entry per source chain; list several tokens in `assets` to accept more than one
// on that chain (they share the same escrow/role addresses — no need to duplicate).
{
network: "eip155:8453", // Base
assets: [
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
"0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", // USDT
],
},
{ network: "tron:mainnet", assets: ["TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"] }, // USDT on Tron
],
markupBps: 300, // allow up to +3% on the source side to cover the spread
quoteDeadlineSeconds: 20,
fulfillmentDeadlineSeconds: 300,
});
// You can also build a corridor by hand if you already hold the addresses — see AtumEscrowCorridor.
// 2. Provide the gateway adapter. The gateway response maps 1:1 onto what the method needs.
// Pass `status` through: it is what distinguishes a payment still settling from one that
// failed. Submit and return — do not poll for completion (see "Settlement outcomes").
const submitter: PaymentSubmitter = {
async submit(request) {
const res = await gateway.payments.submitPayment({ requestBody: request });
return {
payment_id: res.payment_id,
status: res.status,
fulfillment_confirmation: res.fulfillment_confirmation,
};
},
};
// 3. Register the method and create your mppx server. The corridor is NOT part of the server
// config — verification trusts the signed challenge, so one registration serves every corridor.
const method = registerServer({ submitter });
const mppx = Mppx.create({
realm: "api.example.com",
secretKey: process.env.MPP_SECRET_KEY, // recommended: binds each challenge to its contents
methods: [method],
});
// 4. Guard a route. buildChargeChallenge turns your corridor + a chosen source + the price into
// the challenge. Set the price per charge, so one corridor serves any amount.
app.get("/paid-resource", async (req) => {
const { request, meta } = buildChargeChallenge(
corridor,
{ network: "eip155:8453", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }, // offer Base USDC
"10000000", // receive exactly 10 USDC
// Identifies the purchase, NOT the attempt: the same value every time this order is
// re-offered or retried, a different one for a new order. See "Retries and idempotency".
{ intentId: orderIdFor(req) },
);
const result = await mppx.compose(["atum-escrow/charge", { ...request, meta }])(req);
if (result.status === 402) return result.challenge; // not paid yet — ask for payment
return result.withReceipt(new Response("here is your resource")); // paid & settled
});Before production:
mppx.fetch/a singleverify()call is enough for this quickstart, but not for a corridor whose settlement can genuinely take minutes — see Settlement outcomes for handling that without polling.
Quick start — payer (client)
import { Mppx } from "mppx/client";
import { registerClient } from "@atumlabs/mppx-atum-escrow/client";
// The signer is chain-agnostic: pass private-key options (bound to the challenge's source chain
// automatically) or a ready-made SenderSigner. `account` is your source-chain address.
// Use a TESTNET-ONLY wallet for PAYER_PRIVATE_KEY while trying this out — never one holding real funds.
const method = registerClient({
signer: { privateKey: process.env.PAYER_PRIVATE_KEY! },
account: process.env.PAYER_ADDRESS!,
});
const mppx = Mppx.create({ methods: [method] });
// mppx.fetch handles the 402 automatically: it reads the atum-escrow challenge, builds and
// signs the payment, retries with the credential attached, and returns the final 200 response.
const res = await mppx.fetch("https://api.example.com/paid-resource");
const resource = await res.text();Before production:
mppx.fetchretries only a few times with no delay, which isn't enough for a corridor whose settlement can genuinely take minutes — see Settlement outcomes for the retry pattern that handles it.
Approving the source token
On EVM and Tron, the payer must approve the token-transfer contract (Permit2) to move the source
token before paying, or the escrow deposit reverts at settlement. ensureSourceApproval reads the
current allowance and sends an approval only if it falls short (it is a no-op on Solana, which
authorizes the transfer in the signed deposit itself):
import { Wallet, JsonRpcProvider } from "ethers";
import { ensureSourceApproval } from "@atumlabs/mppx-atum-escrow/client";
const wallet = new Wallet(process.env.PAYER_PRIVATE_KEY!, new JsonRpcProvider(RPC_URL));
await ensureSourceApproval({
network: "eip155:8453",
token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
owner: wallet.address,
signer: wallet,
// Pass the source cap this charge needs (from the challenge). A leftover smaller allowance
// then won't be mistaken for enough.
requiredAllowance: BigInt(challenge.request.source.amount),
});Paying from Tron takes a TronWeb instance instead of an ethers signer; everything else is the same, and the Permit2 address for the network is resolved for you:
await ensureSourceApproval({ network: "tron:mainnet", token, owner, tronWeb });The approval waits to be mined before returning, bounded by confirmation.timeoutMs (one minute
by default); a timeout is reported as unconfirmed rather than failed, since the transaction may
still land. onSubmitted hands you the hash the moment it is broadcast.
isUnconfirmed(error) tells the two apart. It matters because the responses are opposite: a
failed approval needs another one, an unconfirmed approval needs a look at the transaction and
nothing else. Sending a second one on top of the first only pays for an allowance you are already
getting.
try {
await ensureSourceApproval({ network, token, owner, signer });
} catch (error) {
if (isUnconfirmed(error)) {
// Broadcast, not yet seen to confirm. Check the hash from onSubmitted; do not re-send.
} else {
throw error;
}
}On EVM, tokens that refuse to overwrite a non-zero allowance — mainnet USDT most notably — are
reset to zero first, automatically and only where the token demands it; the result then carries a
resetTxHash too. Tron does not do this, because an ordinary TRC-20 accepts the change in place.
The approval sent is unlimited, so later charges on the same token need no further transaction.
To bound the approval that gets sent, pass approvalAmount. On its own the amount is treated as
its own requirement, so an allowance still holding the full bound is left alone rather than
re-approved — the right answer for a one-off approval. Across repeated payments, pair it with
requiredAllowance: Permit2 decrements the allowance on every payment, so a bound measured
against itself stops covering itself the moment the first charge lands, and every later call
would send another approval. Either way a bounded approval is consumed as it is spent and
eventually has to be granted again.
These helpers raise an allowance to what a payment needs; they never lower one. A wallet already
holding more than approvalAmount is left exactly as it is and reports alreadySufficient
without sending anything. Reducing or revoking an allowance is a separate operation, and not one
these perform.
needsSourceApproval(params) takes the same arguments and answers the same question without
sending anything, so it is safe to call before every charge:
if (await needsSourceApproval({ network, token, owner, signer })) {
// tell the payer a one-off approval transaction is coming, before asking them to sign
}What the merchant verify() guarantees
verify() fails fast (no chain I/O) before submitting, and throws if any check fails. The Atum
Payment Gateway independently re-validates everything on submit; these checks just reject
obviously-bad input early:
- the source-chain deposit signature recovers to the payer's own
source.account(an EVM/Tron secp256k1 recovery, or a Solana ed25519 verification against the signer's public key); - the signed deposit's token, escrow, and witness roles match the challenge's source option;
max_source_amountdoes not exceed the source cap, and the deposit authorizes exactly that;- the destination account, asset, and
fulfillment_amountare exactly what the merchant advertised (the deposit signature does not bind the receive side, so the method does); - the settlement-routing fields —
escrow_contract_address,quote_selector,fulfillment_proxy, andfulfillment_verifier— match the challenge. These travel in plaintext and aren't covered by the deposit signature, so the method checks them explicitly to stop a payer from redirecting settlement; - the deadlines satisfy
now < quote_deadline < fulfillment_deadline, and do not exceed the challenge's advertised budgets; and (for EVM/Tron) the deposit authorization does not expire beforefulfillment_deadline, so a slow-but-valid settlement never lands on an expired signature.
On success you get an AtumEscrowReceipt — the MPP receipt plus the full settlement confirmation:
{
method: "atum-escrow",
status: "success",
timestamp: string, // settlement time (ISO 8601)
reference: string, // destination-chain transaction hash
fulfillmentConfirmation: { // full settlement details
payment_id: string,
request_id: string,
destination_chain_id: string, // CAIP-2, e.g. "eip155:42161"
destination_tx_hash: string,
// …
}
}verify() resolves with this type, so fulfillmentConfirmation is available straight off the
result. AtumEscrowReceipt is exported, so you can name it in your own function signatures.
Retries and idempotency
A retry must never become a second charge, and two different purchases must never collapse into one. Both come down to a single value: the per-purchase identifier the merchant stamps into the challenge.
const { request, meta } = buildChargeChallenge(corridor, source, amount, {
intentId: order.id, // ← identifies the purchase
})The payer derives the payment's request_id from intentId, and the gateway de-duplicates on that
request_id. So the identifier's lifetime is the definition of "the same payment":
- one value per purchase — a value shared by two purchases makes the second resolve onto the first and go unpaid (see the merchant-side defence below);
- the same value on every retry of that purchase, including every re-issued
402for it — a value that changes per attempt makes each retry a fresh charge; - never derived from the terms — two orders for the same item have identical terms and must still be two payments.
Use the merchant's own order, invoice, or cart id. Notably it is not the challenge id: that is an HMAC over the challenge's own contents, so it moves whenever the expiry or the quote moves, neither of which tracks the purchase.
A challenge carrying no identifier is refused by the payer's client rather than paid. There is no fallback, deliberately: a per-attempt value would look like an idempotency key while letting every retry be charged again.
On top of that, EVM and Tron sources get an on-chain backstop — the deposit nonce is derived from
the same identifier, and the escrow reverts a second deposit reusing it. Solana's replay nonce only
guards a short window, so there the gateway's request_id de-duplication is the durable protection.
The merchant-side defence, and why you want it
Nothing can detect an identifier reused across two genuine purchases — the identifier is the identity, so the second purchase resolves onto the first payment and only one payment is made. As the merchant, you can close that on your own side.
Both purchases resolve to the same payment, so verify() hands you the same receipt for
both — the same paymentId and the same destination transaction. Key fulfilment on the receipt
rather than on the incoming request, and the second purchase is recognised as already-fulfilled
instead of being shipped a second time against a single payment.
Treat this as your responsibility rather than the network's: atum-escrow guarantees you will not be
paid twice for one identifier, and receipt-keyed fulfilment is how you avoid delivering twice for
one.
Reusing an identifier with different terms
The gateway rejects a purchase identifier that comes back with different economics — who pays, who
receives, in which assets, or for how much — instead of silently resolving it to the first payment.
Surface it as a PaymentRejectedError from your submitter and the payer sees the cause; the fix is
always a distinct identifier per purchase.
Settlement outcomes
A payment that settles returns a receipt. A payment that does not settle within the gateway's synchronous window is not a failure, and the two are distinguished because they call for opposite responses:
| verify() outcome | Meaning | What the payer should do |
| --- | --- | --- |
| receipt | settled | nothing — the resource is served |
| SettlementPendingError | accepted, still settling | re-attempt the same purchase; it resolves to this payment and returns its result |
| SettlementFailedError | terminal failure | start a new purchase under a new identifier — this one can never settle |
| PaymentRejectedError | refused, nothing charged | fix the request and pay the same purchase again |
The pending and failed rows call for opposite actions, and the reason is worth stating: a
terminal failure keeps its identifier. The gateway does not release it, so re-attempting that
purchase under the same intentId resolves to the same dead payment for good — which is exactly what
you want for a pending payment and exactly what you must not do after a terminal one. Recovering from
a terminal failure therefore means minting a new intentId; it is a new purchase as far as the
network is concerned. Getting these two the wrong way round is how a payer either charges twice or
waits forever.
What re-attempting a purchase means
Run the purchase again: request a fresh challenge, sign it again, and keep the same intentId.
That split — rebuild the authorization, reuse the identifier — is the whole contract, and each half
matters for a different reason. The intentId must not change because the payment's identity derives
from it; change it and the re-attempt is a second charge rather than a retry. Everything time-bound
must change because quote_deadline and fulfillment_deadline are absolute timestamps, fixed at
the moment the challenge was built.
So a credential you have already signed cannot simply be presented a second time. Once its quote
window has closed, verify() refuses it and says so — and on Solana it is worse than a policy
refusal, because the deposit authorization carries its own on-chain replay window that expires along
with the deadlines.
A payment-enabled fetch handles the rebuild-and-resubmit mechanics for you, since every attempt
fetches a new challenge — but its own retry loop is a fixed, short number of attempts with no delay
between them (3, by default; configurable via maxPaymentRetries, but that only changes how many
times it hammers the endpoint, not how long it waits). That is enough when settlement finishes almost
immediately, but not for a corridor that can genuinely take minutes: raising maxPaymentRetries
turns "retry a few times" into "retry rapidly, still for no longer," which is not the same thing as
waiting for settlement. For that, drive the retry yourself, with a real interval between attempts.
The example below is a payer driving a merchant over real HTTP — two separate processes, not two
functions called back to back — since that is the case mppx.fetch does not cover and this package
provides no shortcut for. method.createCredential (payer) and the merchant's own endpoint are on
opposite ends of the request; nothing here runs the merchant's verify() in the payer's process.
import { Transport } from "mppx/client";
import type { AtumEscrowChallenge } from "@atumlabs/mppx-atum-escrow/client";
// `method` is the client from "Quick start — payer" above. The merchant is what keeps `intentId`
// stable across attempts (it must derive the same value from the order on every request); the
// payer never supplies or sees it directly, only the challenge that already carries it.
const transport = Transport.http();
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
// 1. Ask for the resource. The merchant answers 402 with a fresh challenge.
const firstResponse = await fetch(url);
if (!(await transport.isPaymentRequired(firstResponse))) return firstResponse; // free, or already paid
// 2. Build and sign a payment for THIS challenge. The challenge arrives off the wire, so confirm
// it is this scheme's before signing against it — a bare cast would sign whatever the
// response contained. Each attempt re-signs — a full round trip for a Turnkey/KMS signer — so
// pick an interval below with that cost in mind, not one copied from a lightweight status poller.
const offered = await transport.getChallenge(firstResponse);
if (offered.method !== "atum-escrow" || offered.intent !== "charge") {
throw new Error(`unexpected challenge ${offered.method}.${offered.intent}`);
}
const challenge = offered as AtumEscrowChallenge;
const credential = await method.createCredential({ challenge });
// 3. Resubmit with the credential attached. `response.ok` (200) is the only success case — a
// non-402 failure (e.g. PaymentRejectedError, a 400) is neither settled nor a fresh challenge
// to retry, so check for it explicitly rather than falling into the pending/failed handling
// below, which assumes a 402.
const response = await fetch(url, transport.setCredential({}, credential, { challenge }));
if (response.ok) return response; // paid & settled
if (!(await transport.isPaymentRequired(response))) {
const problem = await response.json();
throw new Error(problem.detail ?? `request failed with ${response.status}`);
}
// Pending and failed are BOTH a 402 — the wire-visible discriminator is the problem-details
// `type`, which the mppx framework's error classes fix per outcome (see isSettlementPending /
// isSettlementFailed below for the merchant-side equivalent check). `paymentId` is its own
// field too, not just interpolated into `detail`'s prose, so the payer can log/reconcile it
// without parsing a sentence.
const problem = await response.json();
if (problem.type !== "https://paymentauth.org/problems/payment-action-required") {
throw new Error(problem.detail); // terminal — a new purchase needs a new intentId
}
// `payment-action-required` covers two different causes: a payment genuinely still settling
// (paymentId present — it reached the gateway) and this attempt's authorization having gone
// stale before it could be submitted (paymentId absent — nothing reached the gateway yet). Log
// accordingly rather than always saying "still settling", which is only true of the first.
console.log(
`attempt ${attempt}: ${problem.paymentId ? `still settling (payment ${problem.paymentId})` : "authorization expired before submission, retrying with a fresh one"}`,
);
await sleep(interval);
}
throw new Error(`purchase never settled after ${maxAttempts} attempts`);Both settlement errors carry the gateway's paymentId for reconciliation — as its own field on
toProblemDetails(), not only interpolated into detail's prose — and both extend the framework's
error types, so a merchant that does not distinguish them still gets the standard 402 +
problem-details response. A same-process merchant discriminates with isSettlementPending /
isSettlementFailed / isPaymentRejected on the thrown error object; a payer, receiving only the
wire response, discriminates on type as shown above.
Because the payer's retry is what collects a pending result, a PaymentSubmitter should submit and
return what the gateway said — not poll for completion. Polling holds the merchant's request
open for the whole settlement window and hides the pending state the retry depends on.
Scope
This version supports the charge intent with EVM, Tron, and Solana source chains and a
merchant-configured destination chain. One source option is offered per challenge; a corridor may
list several and offer a different one per 402.
Solana deadline constraint: a Solana deposit authorization expires within the escrow's fixed on-chain replay window (~2 minutes), independent of the corridor budget. So a corridor that includes a Solana source must keep
fulfillmentDeadlineSecondswithin that window —validateCorridor(run bybuildChargeRequest) enforces this and rejects a longer budget. If you need a longer budget for EVM/Tron sources, put the Solana source in its own corridor.
API
Import everything from the root, or from the role-specific entry points (which expose only what each side needs):
@atumlabs/mppx-atum-escrow— everything below@atumlabs/mppx-atum-escrow/client—registerClient,ensureSourceApproval,needsSourceApproval+ payer types@atumlabs/mppx-atum-escrow/server—registerServer,buildChargeChallenge,buildChargeRequest,validateCorridor,corridorFromDefaults, the settlement errors + merchant types
| Export | Description |
| --- | --- |
| registerClient(config) | Payer-side method. config: { signer, account, now?, solanaClockReader? }. |
| registerServer(config) | Merchant-side method. config: { submitter, now? }. Returns an AtumEscrowServer, whose verify() resolves with an AtumEscrowReceipt. |
| buildChargeChallenge(corridor, select, fulfillmentAmount, { intentId, issuedAt? }) | Builds a charge challenge — the payment terms plus the per-purchase identifier that makes a retry safe. Use this. Returns { request, meta }. |
| buildChargeRequest(corridor, select, fulfillmentAmount, options?) | The payment terms alone, without the identifier. For supplying challenge metadata by hand. |
| validateCorridor(corridor) | Validates a corridor's shape and per-source addresses (run automatically by both builders). |
| corridorFromDefaults(defaults, params) | Builds a corridor by fetching escrow/role/proxy addresses from the gateway /defaults. |
| ensureSourceApproval(params) | Ensures the payer has approved Permit2 to move the source token. signer for EVM, tronWeb for Tron; no-op on Solana. |
| needsSourceApproval(params) | Whether ensureSourceApproval would send a transaction. Read-only, spends no gas. |
| SettlementPendingError, SettlementFailedError, PaymentRejectedError | The non-receipt outcomes of verify(). See Settlement outcomes. |
| isSettlementPending, isSettlementFailed, isPaymentRejected | Guards for the above. |
| atumEscrowChargeMethod | The base mppx method (advanced/custom wiring). |
| ChargeRequestSchema, CredentialPayloadSchema | The zod wire schemas. |
| METHOD_NAME ("atum-escrow"), INTENT ("charge") | The method/intent identifiers. |
| INTENT_ID_META_KEY | The challenge-metadata key carrying the per-purchase identifier. |
Key types: AtumEscrowCorridor, AtumEscrowSource, SenderSigner, SenderSignerOptions,
PaymentSubmitter, PaymentSubmitResult, PaymentSettlementStatus, AtumEscrowClientConfig,
AtumEscrowServer, AtumEscrowServerConfig, ChainDefaultsSource, EnsureApprovalResult,
AtumEscrowChallenge, AtumEscrowCredential, AtumEscrowReceipt, ChargeChallenge, ChargeRequest,
SettlementErrorDetails, PaymentRequest, FulfillmentConfirmation.
EVM implementation note: on EVM the deposit authorization is a Permit2
PermitWitnessTransferFromsignature; Tron uses the equivalent TIP-712 typed data, and Solana uses an ed25519-signed deposit. The public API is the same across all three.
