@metalntwx/metal-pay
v0.2.0
Published
Agent payments on Metal for the official x402 packages: the Metal devnet and testnet as `exact`-scheme networks priced in `*USDZ` for payers and sellers, a one-call payment client with a spending limit, Metal's seller token, the clearing lane for payers w
Readme
@metalntwx/metal-pay
Agent payments on Metal for the official x402 packages: registers the Metal
devnet (eip155:1337) and testnet (eip155:63825) for the exact scheme,
priced in *USDZ, for both sides of a payment, and adds Metal's clearing
lane for payers who pay per call without a transaction each time, and
mandates, which let an agent pay from its owner's account within limits
Metal enforces.
npm install @metalntwx/metal-pay @x402/core viemPayers. createMetalPaymentClient gives an agent a one-call payment
client; registerMetalNetworks(client, { signer, networks? })
registers the scheme on an x402Client you already have. Both refuse,
before signing, a Metal exact offer in any token but *USDZ or under any
EIP-712 domain but USDZ's. createMetalPaymentClient also refuses any offer
above maxAmount, a bigint in *USDZ base units. The default is
DEFAULT_MAX_PAYMENT, 1 *USDZ. It works with @x402/* 2.21 and later.
From 2.23 the stock client pays only tokens in its own table unless its spend
controls list others; createMetalPaymentClient lists *USDZ there, capped
at maxAmount. On a client of your own, add
{ network, asset: USDZ.address } to setSpendControls({ allowedAssets }).
A paid request is never redirected to another origin, so no other site
receives the payment. createMandatePaymentClient does the same.
import { privateKeyToAccount } from "viem/accounts";
import { createMetalPaymentClient } from "@metalntwx/metal-pay";
const { fetchWithPayment } = createMetalPaymentClient({
account: privateKeyToAccount("0x…"),
network: "devnet",
});
const response = await fetchWithPayment("http://127.0.0.1:4021/quote");Payees. registerMetalScheme(server, { networks? }) registers the
stock exact scheme on an x402ResourceServer with metalUsd, the money
parser that prices dollar amounts in *USDZ and names the ERC-3009
message the payer signs (primaryType: "TransferWithAuthorization"); mount it with the official
x402 middleware (@x402/hono, @x402/express). On a server that already
registers ExactEvmScheme for other networks, add
registerMoneyParser(metalUsd(network)) to it instead. Each network's
facilitatorUrl is Metal's x402 facilitator for it:
https://x402-testnet.metalntwx.com/x402/facilitator on testnet, and
http://127.0.0.1:7090/x402/facilitator on devnet, where
just x402-facilitator runs it. It settles exact payments for any
seller; a seller Metal issued a token to sends it with sellerToken(token),
the createAuthHeaders option, and clearing settlement needs one.
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { paymentMiddleware } from "@x402/hono";
import { METAL_NETWORKS, registerMetalScheme } from "@metalntwx/metal-pay";
const facilitator = new HTTPFacilitatorClient({ url: METAL_NETWORKS.devnet.facilitatorUrl });
const server = registerMetalScheme(new x402ResourceServer(facilitator));
app.use(
paymentMiddleware(
{ "GET /quote": { accepts: { scheme: "exact", network: "eip155:1337", payTo: "0x…", price: "0.01" } } },
server,
),
);Clearing. Sellers register ClearingDebitServerScheme next to the exact
scheme and list a clearing-debit offer in a route's accepts; payers pass a
ClearingDebitScheme as createMetalPaymentClient's clearing, which pays
by clearing, and by exact while custody is short or when the seller cannot
take clearing (the facilitator settles clearing only for sellers Metal issued
a token), and fund custody with depositToClearing. Given the facilitator, it
waits until the facilitator counts the deposit, so the next payment can draw
on it. On an x402 client of your own, give it the scheme's selector,
new x402Client(clearing.prefer), and its fetch,
wrapFetchWithPayment(clearing.wrapFetch(fetch), client), so a Debit whose
answer never arrived, or arrived as a bare server error, is presented again
rather than paid twice, for as long as the client runs. withdrawFromClearing takes custody out
once the epoch holding it has closed and passed its challenge window, and says so
when that has not happened yet. @metalntwx/metal-pay/file-storage keeps a
clearing payer's record across restarts (Node only). The facilitator's
README
covers the lane.
Mandates. @metalntwx/metal-pay/mandates lets an agent pay from its
owner's account with a P-256 key of its own, within limits Metal enforces on
chain. createCredential writes the limits (payees, the most per payment, a
budget, how many payments, an expiry) as a Verifiable Intent mandate;
ingestCredential verifies one against the issuers the owner trusts, and
bindCall, replaceCall and revokeCall build the owner's transactions to its own
MetalAccount. Wrap those calls with authorizeMandateCall(client, trustedFactory,
call, signQuorum) to authorize an account self-call, or a list of them as one
batch that runs whole or not at all, with its existing weighted quorum. The
client must name its chain: the digest the quorum signs is computed from it,
never read from the RPC endpoint. The callback supports the same EOA, P-256 and passkey signatures as
ordinary account execution. Factory-created clones share an immutable VI
verifier; budgets and replay protection live in each account. createMandatePaymentClient({ credential, signDigest, network })
is the payer, and MandateExactScheme the same payer as a scheme for an
x402Client of your own: each payment is an ERC-3009 authorization from the
owner's account whose signature is the agent's per-payment mandate, which
the account accepts or refuses through ERC-1271. Sellers and facilitators
see an ordinary exact payment. The client sets no limit of its own.
readCheck names the limit behind a refusal, readMandateState reads the
spend, and pullCall builds a direct payment with no seller. A credential's amounts
are decimal, to the cent. verifyMandate and verifyMandatePayment trust the
MetalAccountFactory METAL_NETWORKS names for the chain, or the
accountFactory given, check the account's clone code against the
factory's current implementation, and match the successful token transfer to the seller's requirements.
The module runs on Node only.
RPC. Neither payers nor payees need one. Each network's rpc is its
default JSON-RPC endpoint, { url } on devnet, or { consoleUrl } on
testnet, where each user signs up in the console and creates an endpoint of
their own. rpcUrlFor(network, override, how) returns the override or the
default, and otherwise throws one line saying how to get an endpoint from the
console and how to pass it.
viem is a peer dependency. Run pnpm test and pnpm typecheck.
