@metalntwx/metal-pay 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/clearing-client.d.ts +168 -0
- package/dist/clearing-client.js +580 -0
- package/dist/clearing-server.d.ts +56 -0
- package/dist/clearing-server.js +81 -0
- package/dist/clearing.d.ts +204 -0
- package/dist/clearing.js +151 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.js +71 -0
- package/dist/file-storage.d.ts +39 -0
- package/dist/file-storage.js +195 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/mandates/account.d.ts +106 -0
- package/dist/mandates/account.js +73 -0
- package/dist/mandates/binding.d.ts +767 -0
- package/dist/mandates/binding.js +171 -0
- package/dist/mandates/chain.d.ts +26 -0
- package/dist/mandates/chain.js +63 -0
- package/dist/mandates/checkout.d.ts +171 -0
- package/dist/mandates/checkout.js +274 -0
- package/dist/mandates/client.d.ts +60 -0
- package/dist/mandates/client.js +124 -0
- package/dist/mandates/extension.d.ts +20 -0
- package/dist/mandates/extension.js +32 -0
- package/dist/mandates/index.d.ts +14 -0
- package/dist/mandates/index.js +14 -0
- package/dist/mandates/ingest.d.ts +22 -0
- package/dist/mandates/ingest.js +26 -0
- package/dist/mandates/jcs.d.ts +12 -0
- package/dist/mandates/jcs.js +17 -0
- package/dist/mandates/l3a.d.ts +73 -0
- package/dist/mandates/l3a.js +158 -0
- package/dist/mandates/seller.d.ts +26 -0
- package/dist/mandates/seller.js +101 -0
- package/dist/mandates/verify.d.ts +92 -0
- package/dist/mandates/verify.js +472 -0
- package/dist/mandates/vi/credential.d.ts +98 -0
- package/dist/mandates/vi/credential.js +409 -0
- package/dist/mandates/vi/issue.d.ts +36 -0
- package/dist/mandates/vi/issue.js +82 -0
- package/dist/mandates/vi/sd-jwt.d.ts +74 -0
- package/dist/mandates/vi/sd-jwt.js +153 -0
- package/dist/networks.d.ts +85 -0
- package/dist/networks.js +102 -0
- package/dist/server.d.ts +40 -0
- package/dist/server.js +55 -0
- 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
|
-
#
|
|
1
|
+
# @metalntwx/metal-pay
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}>;
|