npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@atumlabs/x402-atum-escrow

v0.3.1

Published

The atum-escrow x402 scheme as installable mechanisms: client (payer), server (resource-server 402 builder), facilitator (keyless relay to Atum), and a payment-identifier-aware fetch wrapper.

Readme

Copyright 2026 Atum Labs, Inc.

Licensed under the Apache License, Version 2.0. See the LICENSE file distributed with this package.

The public x402 protocol and third-party x402 packages remain subject to their own terms. This license covers Atum's implementation only.

@atumlabs/x402-atum-escrow

The atum-escrow x402 scheme, as installable mechanisms. Drop it into a standard x402 client or resource server and the pair becomes "Atum-aware": the payer spends any source asset/chain Atum supports, and the merchant receives its chosen asset on its chosen chain, for an exact amount — Atum converts between them. Full documentation: docs.atum.xyz.

atum-escrow rides standard x402 v2 envelopes. The generic parts (catching the 402, parsing accepts[], selecting a requirement, retrying) are handled by the stock x402 HTTP wrappers; this package supplies only the scheme-specific pieces: how the 402 is built (server) and how the PAYMENT-SIGNATURE payload is constructed and signed (client).

Install

pnpm add @atumlabs/x402-atum-escrow

Peer stack: @x402/core, ethers v6 (EVM/Tron signing). Solana signing uses tweetnacl/bs58, bundled with the package.

Roles

Each role is a separate subpath, mirroring the stock scheme packages:

| Subpath | Role | Registers onto | | ---------------------------------------- | ------------------------------------------ | -------------------- | | @atumlabs/x402-atum-escrow/server | resource server (merchant): builds the 402 | x402ResourceServer | | @atumlabs/x402-atum-escrow/client | payer: builds + signs the payment | x402Client | | @atumlabs/x402-atum-escrow/facilitator | facilitator: keyless relay to Atum | x402Facilitator |

Payer (client)              Merchant (server)            Facilitator
   |  GET /resource               |                            |
   | ---------------------------> |                            |
   |  402 + accepts[] (sources),  |                            |
   |  asking for a purchase id    |                            |
   | <--------------------------- |                            |
   | pick a source option, name   |                            |
   | the purchase, sign deposit   |                            |
   |  GET /resource               |                            |
   |  (PAYMENT-SIGNATURE)         |                            |
   | ---------------------------> |  /verify then /settle      |
   |                              | -------------------------> |
   |                              |  settled (+ receipt)       |
   |                              | <------------------------- |
   |  200 + resource              |                            |
   | <--------------------------- |                            |

Naming the purchase is what makes re-attempting it resolve to the original payment instead of a second charge — see Idempotency.

Server (merchant)

Register the scheme onto an x402ResourceServer with your corridor config, then build the route's accepts[] from the same config so there is a single source of truth for which source chains you offer.

import { x402ResourceServer } from "@x402/core/server";
import { paymentMiddleware } from "@x402/express";
import {
  registerAtumEscrowScheme,
  type AtumEscrowServerConfig,
} from "@atumlabs/x402-atum-escrow/server";

const config: AtumEscrowServerConfig = {
  // Where you receive: chain, token, and address.
  destination: {
    network: "eip155:42161",
    asset: "0x…USDC",
    address: "0x…merchant",
  },
  // Exact amount you receive, in atomic units of the destination token.
  fulfillmentAmount: "10000000",
  // Markup over fulfillmentAmount to derive each source cap, in basis points (300 = 3%).
  markupBps: 300,
  fulfillmentProxy: "0x…proxy",
  reserver: "0x…reserver",
  releaser: "0x…releaser",
  quoteDeadlineSeconds: 20,
  fulfillmentDeadlineSeconds: 300,
  // The source chains you accept, keyed by CAIP-2 network. A single instance can serve
  // EVM, Tron, and Solana sources at once; each signs its native deposit authorization.
  sources: {
    // EVM (Permit2 / EIP-712 deposit).
    "eip155:8453": { asset: "0x…baseUSDC", escrow: "0x…baseEscrow" },
    // Tron (TIP-712 deposit; base58 addresses).
    "tron:mainnet": { asset: "T…usdt", escrow: "T…tronEscrow" },
    // Solana (Ed25519 deposit; base58 addresses + the escrow signature-domain).
    "solana:mainnet": {
      asset: "…mint",
      escrow: "…solEscrow",
      svmSignatureClusterId: "mainnet",
      svmSignatureDomainVersion: 1,
    },
  },
};

const server = registerAtumEscrowScheme(
  new x402ResourceServer(facilitatorClient),
  config
);

// One accepts[] entry per source option, from the same config.
const accepts = Object.entries(config.sources).map(([network, source]) => ({
  scheme: "atum-escrow",
  network,
  payTo: source.escrow, // funds lock in escrow, not with the merchant
  price: "$0.10", // required by the route type, but ignored: the amount is fulfillmentAmount + markup
  maxTimeoutSeconds: 60,
}));

app.use(paymentMiddleware({ "GET /paid": { accepts } }, server));

registerAtumEscrowScheme registers under the eip155:* wildcard by default; pass an explicit network list as the third argument to pin it. A malformed config throws at registration (fail-loud at startup).

Which chains and assets can you accept?

The sources map above uses placeholders. The real values come from our documentation: every supported token is listed with its identifier 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 Tron addresses, are case-sensitive, and re-casing one produces a different valid value rather than a different spelling of the same one.

Client (payer)

Register the scheme onto an x402Client with a signer, then wrap fetch. Build the client once and reuse it; each call says which purchase it is paying for.

import { ethers } from "ethers";
import { x402Client } from "@x402/core/client";
import { registerAtumEscrowScheme } from "@atumlabs/x402-atum-escrow/client";
import { wrapFetchWithAtumPayment } from "@atumlabs/x402-atum-escrow/fetch";

const client = new x402Client();
registerAtumEscrowScheme(client, { signer: new ethers.Wallet(PRIVATE_KEY) });

const pay = wrapFetchWithAtumPayment(fetch, client);

// Required. One value per purchase — see Idempotency below.
const res = await pay("https://merchant.example/paid", {}, { paymentIdentifier: orderId });

The client reads the selected 402 option's extra.atum (destination, deadlines, escrow/proxy/role addresses), builds the Atum payment request, signs the source-side escrow-deposit authorization, and emits the PAYMENT-SIGNATURE. The signature is chain-specific: EVM and Tron sign a Permit2/TIP-712 typed-data deposit with an ethers signer; Solana signs an Ed25519 deposit with a SolanaSigner. It registers under eip155:* by default; pass networks to fund from Solana/Tron or to restrict the source chains.

For a Solana source, pass a SolanaSigner — a { publicKey, sign } capability (a local tweetnacl keypair or a remote signer), so key material never reaches the mechanism:

import {
  registerAtumEscrowScheme,
  type SolanaSigner,
} from "@atumlabs/x402-atum-escrow/client";

const solanaSigner: SolanaSigner = {
  publicKey: "…base58",
  async sign(message) {
    /* Ed25519-sign `message`, return the 64-byte detached signature */
  },
};
registerAtumEscrowScheme(client, { signer: solanaSigner, networks: ["solana:mainnet"] });

Precondition: source authorization state

The payer must have the source-side authorization already in place, or the escrow deposit reverts at settlement — arranging it is the payer/app's responsibility (this mechanism assumes it and does not check on-chain):

  • EVM / Tron: an approve(Permit2) allowance covering the spend.
  • Solana: an escrow delegate (created out-of-band) authorizing the deposit.

Do this before signing. A deposit that reverts on-chain is a terminal settlement failure, and a terminal failure belongs to the purchase identifier that caused it — so re-attempting that purchase then needs a new identifier (see Settlement outcomes). Getting the allowance right up front keeps the identifier usable.

For an EVM or Tron source, ensureSourceApproval is offered as an optional convenience — use it if you would rather not write the ERC-20 calls yourself:

import { Wallet, JsonRpcProvider } from "ethers";
import { ensureSourceApproval } from "@atumlabs/x402-atum-escrow/client";

const wallet = new Wallet(PRIVATE_KEY, new JsonRpcProvider(SOURCE_RPC_URL));
await ensureSourceApproval({
  network: "eip155:8453",
  token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
  owner: wallet.address,
  signer: wallet,
  // Pass the amount this charge needs (the 402 option's `amount`), so a leftover smaller
  // allowance isn't mistaken for enough.
  requiredAllowance: 1_000_000n,
});

It reads the current allowance and sends an approval only if it falls short. The approval it sends is unlimited, so later charges on the same token need no further transaction — and it stays sufficient as payments consume it, because Permit2 decrements the allowance each time. On Solana there is no allowance to grant, so the call does nothing.

Paying from Tron takes a TronWeb instance in place of the ethers signer; 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.

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: a bound measured against itself stops covering itself the moment the first charge is decremented from it, 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 as a preflight before asking a payer to sign:

if (await needsSourceApproval({ network, token, owner, signer })) {
  // a one-off approval transaction is due first
}

Idempotency

A retry must not become a second payment. HTTP requests get retried — by an agent, by a proxy, by a user pressing the button again — and each retry re-fetches the resource and gets a fresh 402, so nothing about the rebuilt payment is byte-identical to the first attempt. The scheme gives the payment a stable identity instead, and the Atum gateway de-duplicates on it.

That identity comes from you. You name the purchase being paid for, and the client derives the payment's request_id from that name:

pay(url, {}, { paymentIdentifier: "order-4711" })
   ↓  travels in the standard x402 `payment-identifier` extension
   ↓  client derives request_id from (identifier, source account)
   ↓  gateway de-duplicates on request_id
re-attempt under the same identifier → the ORIGINAL payment, not a second charge

The carrier is the x402 specification's own payment-identifier extension, so nothing about this is specific to Atum: the resource server declares that it needs an identifier, and the payer names the purchase.

The contract

| Rule | Why | | -------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Same identifier for every attempt at one purchase | This is what makes the re-attempt safe. A new identifier is a new payment. | | Different identifier for a different purchase | Reusing one across two genuine purchases de-duplicates the second onto the first — the payer receives twice, the merchant is paid once. | | Nothing can detect a reused identifier | Same as every idempotency-key API. The identifier IS the identity; correctness is the caller's. |

The third rule is the one with teeth, so it is worth being concrete about the failure and the defence. If a payer reuses one identifier across two genuine purchases, the network cannot tell them apart: the second resolves onto the first payment and only one payment is made.

A merchant can close that on its own side, and should. Both purchases resolve to the same payment, so both are served the same receipt — the same payment_id and the same destination_tx_hash. A merchant that keys fulfilment on the receipt rather than on the request therefore fulfils once and recognises the second as already-fulfilled, instead of shipping twice against a single payment. Treat receipt-keyed fulfilment as a merchant responsibility: the scheme guarantees you will not be paid twice for one identifier, and this is how you avoid delivering twice for one.

A natural per-purchase reference — an order id, a cart id, an invoice number — is the right value. A UUID minted once per purchase and reused across that purchase's attempts works equally well. What does not work is a value derived from the payment terms (two identical orders would collide) or one minted fresh per attempt (every re-attempt becomes a new payment).

Any non-empty string is accepted. The extension itself permits 16-128 characters of letters, digits, hyphen and underscore, and a reference outside that — order/0001, or simply one that is too short — is mapped onto a conforming identifier by digest. The mapping is stable, so the same reference always yields the same payment identity; a reference that already conforms travels unchanged, so pass a conforming one if you want the wire value to match your own id.

Both sides

Payer — pass paymentIdentifier on each call. Omitting it is not "opting out of idempotency": the client refuses to sign, naming what to supply. There is deliberately no generated fallback — one would look like an idempotency key while letting every re-attempt double-charge.

Note that the wrapped fetch stays assignable to typeof fetch, so a library that accepts a fetch implementation can call it as fetch(url, init) and drop the third argument. Such a call is refused rather than paid unsafely.

Payer, driving the client directly — if you fetch the 402 yourself and call createPaymentPayload, there is no per-call channel to carry the identifier, so name the purchase on the 402 first:

import { withPaymentIdentifier } from "@atumlabs/x402-atum-escrow/client";

// paymentRequired: the 402 you decoded yourself
const payload = await client.createPaymentPayload(
  withPaymentIdentifier(paymentRequired, orderId),
);

Same function the wrapped fetch uses, so the two paths enforce the same rules: it refuses when neither you nor the resource server named the purchase, and refuses when you and the resource server name it differently (a payer may not overwrite a declared identifier). Writing the extension by hand instead is what to avoid — that is how a payment ends up carrying no identity at all.

Merchant — nothing to write. The server mechanism declares the extension automatically, so a payer knows an identifier is required before it builds anything.

A merchant that already tracks the purchase server-side may name it itself instead, by declaring the identifier on the route:

app.use(
  paymentMiddleware(
    {
      "GET /paid": {
        accepts,
        extensions: {
          "payment-identifier": { info: { required: true, id: "order_4711_a1b2c3d4" } },
        },
      },
    },
    server
  )
);

The scheme never overwrites an identifier the resource server declared — a payer may add to a declaration but not replace it — and a payer that supplies a different one gets an error rather than a payment made under an identity it did not choose. Note that a route declaration is static; naming a purchase per request means declaring it dynamically, which x402 supports through an extension's own enrichDeclaration hook.

Scheme rules

Two behaviours are part of the atum-escrow scheme rather than of any one implementation of it. A third-party client or server built against this scheme has to match them, so they are stated normatively here.

1. A payment with no purchase identifier is refused, not guessed at. The client will not build a payment when neither the caller nor the resource server has named the purchase, and it does not generate an identifier of its own. A generated one would look like an idempotency key while letting every re-attempt become a second payment — the precise failure the identifier exists to prevent. The resource server declares payment-identifier with required: true on every 402, so a payer that knows nothing else about this scheme still learns that an identifier is mandatory and what shape it takes.

2. That refusal surfaces as 402, not 400. A missing identifier is not a malformed request; it is a payment that cannot yet be made safely, and the remedy is for the payer to attempt again with the purchase named. So the resource server answers 402 with the requirement re-declared, and a payer should treat it as "attempt again, naming the purchase" rather than as an error to report and abandon.

Note for implementers: the HTTP status is chosen by the x402 framework, not by the scheme — in @x402/core the mapping is fixed (permit2_allowance_required becomes 412, everything else 402). A scheme therefore cannot select its own status, which is why this is stated as a rule about what a payer should expect rather than as something the scheme enforces.

Settlement outcomes

x402 models settlement as a boolean, so everything short of settled arrives as success: false on a 402. That single flag covers three situations that call for three different responses, and errorReason is what separates them:

| errorReason | What happened | What the payer should do | | -------------------------- | -------------------------------------------- | ----------------------------------------------------------------- | | (none — success: true) | Settled | Nothing; the resource is served | | settlement_pending | Accepted, still settling | Re-attempt under the SAME identifier — it resolves to this payment | | settlement_failed | Terminal failure | Start a new purchase under a NEW identifier — this one can never settle | | a gateway code | Refused; nothing charged | Fix the request and pay the same purchase again |

Three of the four settlement outcomes carry the payment id, so a merchant can reconcile without parsing a message — the refusal is the one exception, because nothing was charged and there is no payment to name:

{
  "success": true,
  "extensions": {
    "atum": { "paymentId": "pay_…", "state": "completed" }
  }
}
{
  "success": false,
  "errorReason": "settlement_pending",
  "extensions": {
    "atum": { "paymentId": "pay_…", "state": "pending", "statusUrl": "https://…" }
  }
}

Pending: retry, don't poll

A pending payment is real and may already be moving funds. Do not release the goods, and do not pay again under a new identifier.

Re-attempting the same purchase — the same paymentIdentifier — is how you collect the result. The gateway resolves the re-attempt onto the same payment and replays its receipt once it has one, so re-attempting costs nothing. statusUrl is there for out-of-band reconciliation (dashboards, support, a ledger job); it is not a step the payment flow has to take, and this client does not poll it.

Note that a pending attempt is not served: the resource is withheld until settlement completes, because goods must not be released against an unfinished payment.

Failed: a retry cannot recover it

settlement_failed is the opposite instruction. That purchase's identifier now resolves to a dead payment forever, so re-attempting it returns the same failure indefinitely. Recovering means treating it as a new purchase, under a new identifier.

This is why the two are distinguished rather than collapsed into one failure: reading pending as failed abandons a payment that was about to succeed (and invites a second charge under a fresh identifier), while reading failed as pending strands the payer in a retry loop that can never finish.

Reusing a key for different terms

The gateway refuses a key that comes back with different economics — a different source, destination, or fulfillmentAmount — instead of silently resolving it onto the first payment. It arrives as a refusal carrying the gateway's code:

{ "success": false, "errorReason": "IDEMPOTENCY_TERMS_MISMATCH", "errorMessage": "…" }

Nothing was charged and nothing is in flight, so this is a request to fix rather than a payment to wait on — the fix is always a distinct key per purchase. Deadlines are deliberately not part of the comparison: every retry re-signs with fresh deadlines, so including them would make the mismatch fire on every legitimate retry.

A transport or server failure is deliberately not reported as a refusal. When the outcome is unknown the response is a 5xx, because calling it a definite "did not settle" would invite a second payment for a purchase that may have gone through.

How pricing works

The merchant pins an exact fulfillmentAmount (what it receives) and adds a static markup to derive each source option's cap (amount). The payer signs a max_source_amount no greater than that cap; the merchant always receives exactly fulfillmentAmount, and the headroom covers settlement margin and gas. A payer may sign a lower cap, which only lowers its own escrow lock and risks the auction not clearing — it can never reduce the merchant's receive side.

Status

The client, server, and facilitator roles are all shipped, across EVM, Solana, and Tron sources. The current profile emits the source-side authorization only; the holder-signed identity binding (a Verifiable Presentation over the request) required for full x402 conformance is added in a later revision.

Further reading