@metalntwx/metal-pay 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -2
  3. package/dist/clearing-client.d.ts +168 -0
  4. package/dist/clearing-client.js +580 -0
  5. package/dist/clearing-server.d.ts +56 -0
  6. package/dist/clearing-server.js +81 -0
  7. package/dist/clearing.d.ts +204 -0
  8. package/dist/clearing.js +151 -0
  9. package/dist/client.d.ts +56 -0
  10. package/dist/client.js +71 -0
  11. package/dist/file-storage.d.ts +39 -0
  12. package/dist/file-storage.js +195 -0
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.js +6 -0
  15. package/dist/mandates/account.d.ts +106 -0
  16. package/dist/mandates/account.js +73 -0
  17. package/dist/mandates/binding.d.ts +767 -0
  18. package/dist/mandates/binding.js +171 -0
  19. package/dist/mandates/chain.d.ts +26 -0
  20. package/dist/mandates/chain.js +63 -0
  21. package/dist/mandates/checkout.d.ts +171 -0
  22. package/dist/mandates/checkout.js +274 -0
  23. package/dist/mandates/client.d.ts +60 -0
  24. package/dist/mandates/client.js +124 -0
  25. package/dist/mandates/extension.d.ts +20 -0
  26. package/dist/mandates/extension.js +32 -0
  27. package/dist/mandates/index.d.ts +14 -0
  28. package/dist/mandates/index.js +14 -0
  29. package/dist/mandates/ingest.d.ts +22 -0
  30. package/dist/mandates/ingest.js +26 -0
  31. package/dist/mandates/jcs.d.ts +12 -0
  32. package/dist/mandates/jcs.js +17 -0
  33. package/dist/mandates/l3a.d.ts +73 -0
  34. package/dist/mandates/l3a.js +158 -0
  35. package/dist/mandates/seller.d.ts +26 -0
  36. package/dist/mandates/seller.js +101 -0
  37. package/dist/mandates/verify.d.ts +92 -0
  38. package/dist/mandates/verify.js +472 -0
  39. package/dist/mandates/vi/credential.d.ts +98 -0
  40. package/dist/mandates/vi/credential.js +409 -0
  41. package/dist/mandates/vi/issue.d.ts +36 -0
  42. package/dist/mandates/vi/issue.js +82 -0
  43. package/dist/mandates/vi/sd-jwt.d.ts +74 -0
  44. package/dist/mandates/vi/sd-jwt.js +153 -0
  45. package/dist/networks.d.ts +85 -0
  46. package/dist/networks.js +102 -0
  47. package/dist/server.d.ts +40 -0
  48. package/dist/server.js +55 -0
  49. package/package.json +57 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nexus Networks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # @metalntwx/metal-pay
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Agent payments on Metal for the official x402 packages: registers the Metal
4
+ devnet (`eip155:1337`) and testnet (`eip155:63825`) for the `exact` scheme,
5
+ priced in `*USDZ`, for both sides of a payment, and adds Metal's clearing
6
+ lane for payers who pay per call without a transaction each time, and
7
+ mandates, which let an agent pay from its owner's account within limits
8
+ Metal enforces.
9
+
10
+ ```bash
11
+ npm install @metalntwx/metal-pay @x402/core viem
12
+ ```
13
+
14
+ **Payers.** `createMetalPaymentClient` gives an agent a one-call payment
15
+ client; `registerMetalNetworks(client, { signer, networks? })`
16
+ registers the scheme on an `x402Client` you already have. Both refuse,
17
+ before signing, a Metal `exact` offer in any token but `*USDZ` or under any
18
+ EIP-712 domain but USDZ's. `createMetalPaymentClient` also refuses any offer
19
+ above `maxAmount`, a `bigint` in `*USDZ` base units. The default is
20
+ `DEFAULT_MAX_PAYMENT`, 1 `*USDZ`. It works with `@x402/*` 2.21 and later.
21
+ From 2.23 the stock client pays only tokens in its own table unless its spend
22
+ controls list others; `createMetalPaymentClient` lists `*USDZ` there, capped
23
+ at `maxAmount`. On a client of your own, add
24
+ `{ network, asset: USDZ.address }` to `setSpendControls({ allowedAssets })`.
25
+
26
+ ```typescript
27
+ import { privateKeyToAccount } from "viem/accounts";
28
+ import { createMetalPaymentClient } from "@metalntwx/metal-pay";
29
+
30
+ const { fetchWithPayment } = createMetalPaymentClient({
31
+ account: privateKeyToAccount("0x…"),
32
+ network: "devnet",
33
+ });
34
+ const response = await fetchWithPayment("http://127.0.0.1:4021/quote");
35
+ ```
36
+
37
+ **Payees.** `registerMetalScheme(server, { networks? })` registers the
38
+ stock `exact` scheme on an `x402ResourceServer` with `metalUsd`, the money
39
+ parser that prices dollar amounts in `*USDZ` and names the ERC-3009
40
+ message the payer signs (`primaryType: "TransferWithAuthorization"`); mount it with the official
41
+ x402 middleware (`@x402/hono`, `@x402/express`). On a server that already
42
+ registers `ExactEvmScheme` for other networks, add
43
+ `registerMoneyParser(metalUsd(network))` to it instead. Each network's
44
+ `facilitatorUrl` is Metal's x402 facilitator for it:
45
+ `https://x402-testnet.metalntwx.com/x402/facilitator` on testnet, and
46
+ `http://127.0.0.1:7090/x402/facilitator` on devnet, where
47
+ `just x402-facilitator` runs it. It settles `exact` payments for any
48
+ seller; a seller Metal issued a token to sends it with `sellerToken(token)`,
49
+ the `createAuthHeaders` option, and clearing settlement needs one.
50
+
51
+ ```typescript
52
+ import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
53
+ import { paymentMiddleware } from "@x402/hono";
54
+ import { METAL_NETWORKS, registerMetalScheme } from "@metalntwx/metal-pay";
55
+
56
+ const facilitator = new HTTPFacilitatorClient({ url: METAL_NETWORKS.devnet.facilitatorUrl });
57
+ const server = registerMetalScheme(new x402ResourceServer(facilitator));
58
+ app.use(
59
+ paymentMiddleware(
60
+ { "GET /quote": { accepts: { scheme: "exact", network: "eip155:1337", payTo: "0x…", price: "0.01" } } },
61
+ server,
62
+ ),
63
+ );
64
+ ```
65
+
66
+ **Clearing.** Sellers register `ClearingDebitServerScheme` next to the `exact`
67
+ scheme and list a `clearing-debit` offer in a route's `accepts`; payers pass a
68
+ `ClearingDebitScheme` as `createMetalPaymentClient`'s `clearing`, which pays
69
+ by clearing, and by `exact` while custody is short or when the seller cannot
70
+ take clearing (the facilitator settles clearing only for sellers Metal issued
71
+ a token), and fund custody with `depositToClearing`. Given the facilitator, it
72
+ waits until the facilitator counts the deposit, so the next payment can draw
73
+ on it. On an x402 client of your own, give it the scheme's selector,
74
+ `new x402Client(clearing.prefer)`, and its fetch,
75
+ `wrapFetchWithPayment(clearing.wrapFetch(fetch), client)`, so a Debit whose
76
+ answer never arrived, or arrived as a bare server error, is presented again
77
+ rather than paid twice, for as long as the client runs. `withdrawFromClearing` takes custody out
78
+ once the epoch holding it has closed and passed its challenge window, and says so
79
+ when that has not happened yet. `@metalntwx/metal-pay/file-storage` keeps a
80
+ clearing payer's record across restarts (Node only). The facilitator's
81
+ [README](https://github.com/mx56db/metal/tree/main/apps/x402-facilitator)
82
+ covers the lane.
83
+
84
+ **Mandates.** `@metalntwx/metal-pay/mandates` lets an agent pay from its
85
+ owner's account with a P-256 key of its own, within limits Metal enforces on
86
+ chain. `createCredential` writes the limits (payees, the most per payment, a
87
+ budget, how many payments, an expiry) as a Verifiable Intent mandate;
88
+ `ingestCredential` verifies one against the issuers the owner trusts, and
89
+ `bindCall`, `replaceCall` and `revokeCall` build the owner's transactions to its own
90
+ MetalAccount. Wrap those calls with `authorizeMandateCall(client, trustedFactory,
91
+ call, signQuorum)` to authorize an account self-call with its existing weighted
92
+ quorum. The callback supports the same EOA, P-256 and passkey signatures as
93
+ ordinary account execution. Factory-created clones share an immutable VI
94
+ verifier; budgets and replay protection live in each account. `createMandatePaymentClient({ credential, signDigest, network })`
95
+ is the payer, and `MandateExactScheme` the same payer as a scheme for an
96
+ `x402Client` of your own: each payment is an ERC-3009 authorization from the
97
+ owner's account whose signature is the agent's per-payment mandate, which
98
+ the account accepts or refuses through ERC-1271. Sellers and facilitators
99
+ see an ordinary `exact` payment. The client sets no limit of its own.
100
+ `readCheck` names the limit behind a refusal, `readMandateState` reads the
101
+ spend, and `pullCall` builds a direct payment with no seller. Amounts are
102
+ whole cents. `verifyMandate` and `verifyMandatePayment` trust the
103
+ MetalAccountFactory `METAL_NETWORKS` names for the chain, or the
104
+ `accountFactory` given, authenticate clone code at the binding and settlement
105
+ blocks, and match the successful token transfer to the seller's requirements.
106
+ The module runs on Node only.
107
+
108
+ **RPC.** Neither payers nor payees need one. Each network's `rpc` is its
109
+ default JSON-RPC endpoint, `{ url }` on devnet, or `{ consoleUrl }` on
110
+ testnet, where each user signs up in the console and creates an endpoint of
111
+ their own. `rpcUrlFor(network, override, how)` returns the override or the
112
+ default, and otherwise throws one line saying how to get an endpoint from the
113
+ console and `how` to pass it.
114
+
115
+ `viem` is a peer dependency. Run `pnpm test` and `pnpm typecheck`.
@@ -0,0 +1,168 @@
1
+ import type { PaymentPayloadResult, PaymentRequirements, SchemeClientHooks, SchemeNetworkClient } from "@x402/core/types";
2
+ import { type Address, type Hex, type LocalAccount } from "viem";
3
+ /** What signs: a viem local account, or anything with an address and
4
+ * EIP-712 `signTypedData`. */
5
+ export interface ClearingSigner {
6
+ readonly address: Address;
7
+ signTypedData(args: {
8
+ domain: Record<string, unknown>;
9
+ types: Record<string, unknown>;
10
+ primaryType: string;
11
+ message: Record<string, unknown>;
12
+ }): Promise<Hex>;
13
+ }
14
+ export interface ClearingDebitSchemeOptions {
15
+ /** The facilitator this payer trusts to report its position: Metal's,
16
+ * `https://<facilitator host>/x402/facilitator`. */
17
+ facilitator: string;
18
+ /** Where the highest total signed each epoch is remembered. Default: in
19
+ * memory, which does not survive a restart. */
20
+ storage?: ClearingPositionStorage;
21
+ fetch?: typeof globalThis.fetch;
22
+ /** Clock, in milliseconds. Default: wall time. */
23
+ now?: () => number;
24
+ }
25
+ /** The payer's position as stored: the epoch, the next debit index and the
26
+ * running total it last signed. Decimal strings, so any store can hold it. */
27
+ export interface StoredClearingPosition {
28
+ epoch: string;
29
+ /** The next debit index, after the debit signed last. */
30
+ index: string;
31
+ /** The running total of the debit signed last. */
32
+ cumulativeDebit: string;
33
+ /** The highest running total ever signed in this epoch. It never goes
34
+ * down, so a stale or low report cannot weaken the check made against it. */
35
+ highestSigned: string;
36
+ }
37
+ /** Where positions are kept: one record per network, Clearing contract,
38
+ * payer and epoch. */
39
+ export interface ClearingPositionStorage {
40
+ get(key: string): Promise<StoredClearingPosition | undefined>;
41
+ /** Store the position. An implementation MUST keep the larger of the
42
+ * stored and the new `highestSigned`, atomically with the write: two
43
+ * clients sharing a store sign in between each other's reads and writes,
44
+ * and the mark must never move down. `keepHighest` does the merge. */
45
+ set(key: string, position: StoredClearingPosition): Promise<void>;
46
+ }
47
+ /** `next`, with `highestSigned` no lower than the one already stored. */
48
+ export declare function keepHighest(existing: StoredClearingPosition | undefined, next: StoredClearingPosition): StoredClearingPosition;
49
+ /** The default store: positions do not survive a process restart. */
50
+ export declare class InMemoryClearingPositionStorage implements ClearingPositionStorage {
51
+ private readonly positions;
52
+ get(key: string): Promise<StoredClearingPosition | undefined>;
53
+ set(key: string, position: StoredClearingPosition): Promise<void>;
54
+ }
55
+ export declare class ClearingDebitScheme implements SchemeNetworkClient {
56
+ private readonly signer;
57
+ readonly scheme = "clearing-debit";
58
+ readonly schemeHooks: SchemeClientHooks;
59
+ private readonly facilitator;
60
+ private readonly storage;
61
+ private readonly doFetch;
62
+ private readonly now;
63
+ /** Per payer and contract: the facilitator's last answer, adjusted for
64
+ * what this client has signed since. */
65
+ private readonly positions;
66
+ /** Per payer and contract: until when to pay the standard way. */
67
+ private readonly shortUntil;
68
+ /** Sellers the facilitator settles no clearing for, by network, contract
69
+ * and payee: a seller without Metal's seller token. That holds for as long
70
+ * as the client runs, so they are paid the standard way from then on. */
71
+ private readonly standardOnly;
72
+ /** Per payer and contract: the Debits whose outcome is in doubt, each to
73
+ * be presented again for its own offer. Only an answer about that Debit
74
+ * clears it. */
75
+ private readonly unresolved;
76
+ constructor(signer: ClearingSigner, options: ClearingDebitSchemeOptions);
77
+ /** The selector to give `new x402Client(...)`: the clearing offer, unless
78
+ * custody was lately found short or the seller cannot take clearing, in
79
+ * which case the standard one. A Debit whose outcome is in doubt is always
80
+ * presented again by clearing. */
81
+ readonly prefer: (_x402Version: number, accepts: PaymentRequirements[]) => PaymentRequirements;
82
+ /** One payment at a time per payer and contract: reading the position,
83
+ * signing and recording it must not interleave, or two payments would sign
84
+ * the same index. */
85
+ private readonly queues;
86
+ private exclusive;
87
+ private key;
88
+ private sellerOf;
89
+ /** The fetch to give `wrapFetchWithPayment`. A request carrying a Debit
90
+ * that fails without an answer, or ends in a server error that says
91
+ * nothing about the Debit (a proxy's 502 or 504), may still have been
92
+ * admitted, so its Debit is held in doubt: the next payment for that offer
93
+ * presents it again. It is held in memory, for as long as the client
94
+ * runs. */
95
+ wrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch;
96
+ /** Holds the Debit a request carried, if it carried one of this client's. */
97
+ private holdSent;
98
+ private hold;
99
+ /** Clears a Debit once a seller has answered about it. */
100
+ private release;
101
+ /** What makes two offers the same payment: a Debit signed for one can be
102
+ * presented again for the other. */
103
+ private offerId;
104
+ /** Where the next Debit starts: the facilitator's answer while it is
105
+ * fresh, else a new one. */
106
+ private position;
107
+ createPaymentPayload(x402Version: number, requirements: PaymentRequirements): Promise<PaymentPayloadResult>;
108
+ private sign;
109
+ }
110
+ /** Put funds into clearing custody. `from` pays and sends the transactions:
111
+ * the agent's own wallet, or anyone funding it, in which case `account` is
112
+ * the agent's address. The tokens go from `from` straight into custody under
113
+ * `account`; only `account`'s signature can spend or withdraw them.
114
+ *
115
+ * Two transactions (an approval for exactly this amount, unless that much
116
+ * is already approved, then the deposit), paid for in the network's fee token by `from`. Gas and
117
+ * fees are set here because a Metal node cannot be asked to estimate them.
118
+ *
119
+ * The operator counts a deposit some seconds after its receipt, and refuses
120
+ * clearing payments against it until then. Given `facilitator`, this waits
121
+ * until the facilitator reports the deposit in the payer's position, so the
122
+ * next payment can draw on it. */
123
+ export declare function depositToClearing(opts: {
124
+ rpcUrl: string;
125
+ chainId: number;
126
+ from: LocalAccount;
127
+ /** The Clearing contract and the token it holds, as the 402 names them:
128
+ * `extra.clearing.contract` and `asset`. */
129
+ contract: Address;
130
+ asset: Address;
131
+ /** Whose custody is credited. Default: `from` itself. */
132
+ account?: Address;
133
+ amount: bigint;
134
+ /** The facilitator to wait on until it counts the deposit, as for
135
+ * `ClearingDebitScheme`. Only `from`'s own custody can be waited on: the
136
+ * position request is signed by the account it is about. */
137
+ facilitator?: string;
138
+ /** How long to wait for the facilitator to count it. Default: 60 s. */
139
+ timeoutMs?: number;
140
+ /** How often to ask. Default: every second. */
141
+ pollMs?: number;
142
+ fetch?: typeof globalThis.fetch;
143
+ }): Promise<{
144
+ approveTx: Hex | null;
145
+ depositTx: Hex;
146
+ available?: bigint;
147
+ }>;
148
+ /** Take funds out of clearing custody: a payer's unused deposit, or what a
149
+ * seller has been paid. It asks the facilitator what can be withdrawn, signs
150
+ * an Exit for that amount, and submits it; Metal's operator sends the two
151
+ * transactions (request, then release) and pays their gas.
152
+ *
153
+ * Only this wallet's signature moves its funds, and only to the destination
154
+ * it names, which defaults to the wallet itself. */
155
+ export declare function withdrawFromClearing(signer: ClearingSigner, facilitator: string, options: {
156
+ network: string;
157
+ /** Where the funds go. Default: the wallet itself. */
158
+ destination?: Address;
159
+ /** How much to withdraw, in base units. Default: everything available. */
160
+ amount?: bigint;
161
+ fetch?: typeof globalThis.fetch;
162
+ /** Clock, in milliseconds. Default: wall time. */
163
+ now?: () => number;
164
+ }): Promise<{
165
+ amount: bigint;
166
+ requestTx: string;
167
+ releaseTx: string | null;
168
+ }>;