@metalntwx/metal-agent 0.2.0 → 0.3.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.
@@ -59,7 +59,7 @@ export async function pay(target, account, url, limit, print, request = {}) {
59
59
  */
60
60
  export async function payUnderMandate(target, payer, url, print, request = {}) {
61
61
  const mandate = await loadMandate(payer.credentialPath);
62
- const signDigest = await mandateAgentSigner(mandate, payer.agentKeyPath);
62
+ const signDigest = await mandateAgentSigner(mandate, payer.credentialPath, payer.mandateKeyFile);
63
63
  const paying = createMandatePaymentClient({ credential: mandate.credential, signDigest, network: target.network });
64
64
  print(`mandate ${paying.reference}`);
65
65
  await paidRequest(target, { ...paying, payer: paying.principal }, url, print, request);
package/dist/help.js CHANGED
@@ -18,8 +18,8 @@ const COMMANDS = [
18
18
  },
19
19
  {
20
20
  name: "pay",
21
- usage: `metal-agent pay <url> ${NETWORK} [--key <file>] [--max <usdz>]\n [--method <method>] [--body <text>] [--header 'name: value']...\nmetal-agent pay <url> --mandate <credential> ${NETWORK} [--agent-key <file>]`,
22
- about: "Requests the URL and pays its x402 offer in *USDZ. With --key, the agent pays from its own key and refuses, before signing, any offer above --max (1 *USDZ by default). With --mandate, it pays from the owner's account within the mandate's limits, signed by the agent's mandate key, the <credential>.agent-key.json beside the credential unless --agent-key names another. --method (GET by default, POST with a --body), --body (application/json unless a --header names a content type) and each --header are sent again with the payment.",
21
+ usage: `metal-agent pay <url> ${NETWORK} [--key <file>] [--max <usdz>]\n [--method <method>] [--body <text>] [--header 'name: value']...\nmetal-agent pay <url> --mandate <credential> ${NETWORK} [--mandate-key-file <file>]`,
22
+ about: "Requests the URL and pays its x402 offer in *USDZ. With --key, the agent pays from its own key and refuses, before signing, any offer above --max (1 *USDZ by default). With --mandate, it pays from the owner's account within the mandate's limits, signed by the .key.json file in the credential's directory whose key the credential names, unless --mandate-key-file names another. --method (GET by default, POST with a --body), --body (application/json unless a --header names a content type) and each --header are sent again with the payment.",
23
23
  },
24
24
  {
25
25
  name: "status",
@@ -28,23 +28,23 @@ const COMMANDS = [
28
28
  },
29
29
  {
30
30
  name: "mcp",
31
- usage: "metal-agent mcp [--key <file>] [--max <usdz>]\nmetal-agent mcp --mandate <credential> [--agent-key <file>]",
31
+ usage: "metal-agent mcp [--key <file>] [--max <usdz>]\nmetal-agent mcp --mandate <credential> [--mandate-key-file <file>]",
32
32
  about: "Serves pay, fund and status as MCP tools over stdio. What pay spends is fixed here, by whoever starts the server: its key and --max, or one mandate. The model calling the tools reads pages and offers anyone can write, so it can choose no other key, no mandate and no higher limit; under a key, a max it passes can only lower the server's.",
33
33
  },
34
34
  {
35
35
  name: "account create",
36
- usage: `metal-agent account create --out <owner key file to create> | --key <owner key file> ${NETWORK}`,
37
- about: "Names the owner's Metal account, a smart account the owner's key controls, and prints its address, the --principal of the owner's credentials. It sends nothing: the owner's first bind creates the account on chain. Keep the owner's key where no agent can read it.",
36
+ usage: `metal-agent account create --out <owner key file> | --key <owner key file> ${NETWORK}`,
37
+ about: "Creates the owner's Metal account, a smart account the owner's key controls, and prints its address, the --principal of the owner's credentials. The owner's key pays the gas; one that holds none is funded first, from the faucet on testnet. An account that exists is named, and nothing is sent. Keep the owner's key where no agent can read it.",
38
38
  },
39
39
  {
40
40
  name: "mandate credential create",
41
- usage: `metal-agent mandate credential create --principal <address> --cap <usdz> --budget <usdz> --out <file>\n --key <owner key file> | --issuer-key <file> ${NETWORK}\n [--payee <address>[,<name>[,<website>]]]... [--expires-in <seconds>] [--max-occurrences <n>]\n [--description <text>] [--agent-key <file>]`,
42
- about: "Writes the limits an agent pays within into a credential file: the most per payment (--cap), the most in total over the mandate's whole life (--budget, which never resets), an expiry (--expires-in, 30 days by default) and, when --payee names any, the addresses it may pay, each with an optional name and website. Amounts are in *USDZ, to the cent. The agent's mandate key and the gas key for its pulls are written beside the credential unless --agent-key names a mandate key the agent already has. The keys that sign the credential are the owner's: they live beside the owner's key file, <key>.issuer.json, created on first use, unless --issuer-key names their file.",
41
+ usage: `metal-agent mandate credential create --principal <address> --cap <usdz> --budget <usdz> --out <file>\n --key <owner key file> | --issuer-key <file> ${NETWORK}\n [--payee <address>[,<name>[,<website>]]]... [--expires-in <seconds>] [--starts-in <seconds>] [--max-occurrences <n>]\n [--description <text>] [--mandate-key-file <file>]`,
42
+ about: "Writes the limits an agent pays within into a credential file: the most per payment (--cap), the most in total over the mandate's whole life (--budget, which never resets), an expiry (--expires-in, 30 days by default), a start (--starts-in, at once by default) and, when --payee names any, the addresses it may pay, each with an optional name and website. Amounts are in *USDZ, to the cent. The mandate key the agent signs with is written beside the credential, mandate.key.json for --out mandate.json, unless --mandate-key-file names one that exists, as for a replacement. The keys that sign the credential are the owner's: they live beside the owner's key file, <key>.issuer.json, created on first use, unless --issuer-key names their file.",
43
43
  },
44
44
  {
45
45
  name: "mandate bind",
46
46
  usage: `metal-agent mandate bind --credential <file> --key <owner key file> ${NETWORK} [--issuer <file>]... [--signer-key <file>]...`,
47
- about: "Binds the credential on the owner's account, which then pays under it. The account's quorum signs: --key pays the gas and is the sole signer unless repeated --signer-key names the quorum's keys. --issuer names the issuers whose credentials the owner accepts, the <key>.issuer.public.json beside the owner's key by default.",
47
+ about: "Binds the credential on the owner's account, which then pays under it. The account's owners sign: --key pays the gas and is the sole signer unless repeated --signer-key names the owners' keys. --issuer names the issuers whose credentials the owner accepts, the <key>.issuer.public.json beside the owner's key by default.",
48
48
  },
49
49
  {
50
50
  name: "mandate replace",
@@ -68,21 +68,22 @@ const COMMANDS = [
68
68
  },
69
69
  {
70
70
  name: "mandate pull",
71
- usage: `metal-agent mandate pull --credential <file> --to <address> --amount <usdz> ${NETWORK}\n [--key <file>] [--agent-key <file>] [--record-refusal]`,
72
- about: "Pays an address the mandate allows, with no seller. The --key account, the agent's gas key by default, submits it and pays its gas. A payout the mandate would refuse is reported and not sent, and the command exits 1; --record-refusal sends it so the refusal is recorded on chain. It prints the transaction as soon as it is sent; if the receipt never arrives, check it with metal-agent status before pulling again.",
71
+ usage: `metal-agent mandate pull --credential <file> --to <address> --amount <usdz> ${NETWORK}\n [--key <file>] [--mandate-key-file <file>] [--record-refusal]`,
72
+ about: "Pays an address the mandate allows, with no seller. The facilitator submits it and pays its gas, unless --key names a funded key to submit it. A payout the mandate would refuse is reported and not sent, and the command exits 1; with --key, --record-refusal sends it so the refusal is recorded on chain. The outcome is read from the chain: paid, refused, held in the payee's escrow, or pending. A pending pull is kept beside the credential, and running the same command again sends that same pull, which pays at most once.",
73
73
  },
74
74
  {
75
75
  name: "mandate verify",
76
76
  usage: `metal-agent mandate verify --credential <file> --issuer <file>... ${NETWORK} [--tx <hash> [--signed-offer <text>]]`,
77
- about: "Checks the mandate and every payment under it from the chain alone, or one --tx: the binding, the credential against it and under an --issuer you trust, and each payment as the agent key's signed per-payment mandate. With --signed-offer, the order the agent signed for that payment, as pay printed it. A check that fails exits 1.",
77
+ about: "Checks the mandate and every payment under it from the chain alone, or one --tx: the binding, the credential against it and under an --issuer you trust, and each payment as the mandate key's signed per-payment mandate. With --signed-offer, the order the agent signed for that payment, as pay printed it. A check that fails exits 1.",
78
78
  },
79
79
  ];
80
- const SHARED = `Chain commands also take --rpc-url and --faucet-url, which override
81
- METAL_RPC_URL and METAL_FAUCET_URL, which override each network's defaults.
80
+ const SHARED = `Chain commands also take --rpc-url and --faucet-url, and mandate pull
81
+ --facilitator-url, which override METAL_RPC_URL, METAL_FAUCET_URL and
82
+ METAL_FACILITATOR_URL, which override each network's defaults.
82
83
  On testnet, status, account create and the mandate commands other than
83
84
  credential create need an RPC endpoint of your own:
84
- sign up in the Metal console and create one. account create and the mandate
85
- commands take --factory <address> when the network's MetalAccountFactory is
85
+ sign up in the Metal console and create one. account create and mandate bind,
86
+ replace, renew, revoke, status and verify take --factory <address> when the network's MetalAccountFactory is
86
87
  not the one metal-agent knows.
87
88
 
88
89
  metal-agent <command> --help explains one command.`;
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { credentialCreate, accountCreate, mandateBind, mandatePull, mandateRenew
9
9
  import { parseHeaders, parseMax, pay, payUnderMandate } from "./commands/pay.js";
10
10
  import { status } from "./commands/status.js";
11
11
  import { helpFor } from "./help.js";
12
- import { DEFAULT_KEY_PATH, agentKeyPath, gasKeyPath, issuerKeyPath, publicKeyPath, readKey } from "./keystore.js";
12
+ import { DEFAULT_KEY_PATH, issuerKeyPath, publicKeyPath, readKey } from "./keystore.js";
13
13
  import { mcpAuthority, serveMcp } from "./mcp.js";
14
14
  const print = (line) => console.log(line);
15
15
  async function main(argv) {
@@ -20,6 +20,7 @@ async function main(argv) {
20
20
  network: { type: "string" },
21
21
  "rpc-url": { type: "string" },
22
22
  "faucet-url": { type: "string" },
23
+ "facilitator-url": { type: "string" },
23
24
  key: { type: "string" },
24
25
  factory: { type: "string" },
25
26
  "signer-key": { type: "string", multiple: true },
@@ -33,13 +34,14 @@ async function main(argv) {
33
34
  credential: { type: "string" },
34
35
  replaces: { type: "string" },
35
36
  issuer: { type: "string", multiple: true },
36
- "agent-key": { type: "string" },
37
+ "mandate-key-file": { type: "string" },
37
38
  "issuer-key": { type: "string" },
38
39
  principal: { type: "string" },
39
40
  payee: { type: "string", multiple: true },
40
41
  cap: { type: "string" },
41
42
  budget: { type: "string" },
42
43
  "expires-in": { type: "string" },
44
+ "starts-in": { type: "string" },
43
45
  "max-occurrences": { type: "string" },
44
46
  tx: { type: "string" },
45
47
  "signed-offer": { type: "string" },
@@ -50,6 +52,10 @@ async function main(argv) {
50
52
  },
51
53
  });
52
54
  const [command, sub] = positionals;
55
+ const extra = positionals.slice(argumentCount(command, sub));
56
+ if (extra.length > 0) {
57
+ throw new Error(`unexpected argument${extra.length > 1 ? "s" : ""} ${extra.map((a) => JSON.stringify(a)).join(" ")}; quote a value that holds a space`);
58
+ }
53
59
  if (values.help || command === undefined || command === "help") {
54
60
  print(helpFor(command === "help" ? positionals.slice(1) : positionals.slice(0, 3)));
55
61
  return command === undefined && !values.help ? 2 : 0;
@@ -57,6 +63,7 @@ async function main(argv) {
57
63
  const on = () => target(values.network, {
58
64
  rpcUrl: values["rpc-url"],
59
65
  faucetUrl: values["faucet-url"],
66
+ facilitatorUrl: values["facilitator-url"],
60
67
  });
61
68
  switch (command) {
62
69
  case "key":
@@ -74,12 +81,14 @@ async function main(argv) {
74
81
  const request = { method: values.method, body: values.body, headers: parseHeaders(values.header, "--header") };
75
82
  if (values.mandate !== undefined) {
76
83
  if (values.max !== undefined || values.key !== undefined) {
77
- throw new Error("pay --mandate takes neither --max nor --key: the mandate sets the limit, and the agent's key is --agent-key");
84
+ throw new Error("pay --mandate takes neither --max nor --key: the mandate sets the limit, and the agent signs with its mandate key");
78
85
  }
79
- const payer = { credentialPath: values.mandate, agentKeyPath: values["agent-key"] ?? agentKeyPath(values.mandate) };
86
+ const payer = { credentialPath: values.mandate, mandateKeyFile: values["mandate-key-file"] };
80
87
  await payUnderMandate(on(), payer, sub, print, request);
81
88
  return 0;
82
89
  }
90
+ if (values["mandate-key-file"] !== undefined)
91
+ throw new Error("--mandate-key-file goes with --mandate");
83
92
  const limit = { maxAmount: parseMax(values.max, "--max"), raise: "pass --max to allow it" };
84
93
  await pay(on(), await readKey(values.key ?? DEFAULT_KEY_PATH), sub, limit, print, request);
85
94
  return 0;
@@ -88,7 +97,7 @@ async function main(argv) {
88
97
  if (sub !== "create")
89
98
  throw new Error(`unknown account subcommand "${sub ?? ""}"; try: account create`);
90
99
  if ((values.key === undefined) === (values.out === undefined))
91
- throw new Error("account create needs --out, the owner key file to create, or --key, one that exists");
100
+ throw new Error("account create needs --out, the owner key file, created when it does not exist, or --key, one that exists");
92
101
  const factory = accountFactoryFor(on().network, values.factory === undefined ? undefined : parseAddress(values.factory, "--factory"), "pass --factory");
93
102
  await accountCreate(on(), { accountFactory: factory, ...(values.key === undefined ? { out: values.out } : { keyPath: values.key }) }, print);
94
103
  return 0;
@@ -116,10 +125,11 @@ async function main(argv) {
116
125
  cap: parseCents(values.cap, "--cap"),
117
126
  budget: parseCents(values.budget, "--budget"),
118
127
  expiresIn: parseCount(values["expires-in"], "--expires-in") ?? 30 * 86_400,
128
+ startsIn: parseCount(values["starts-in"], "--starts-in"),
119
129
  maxOccurrences: parseCount(values["max-occurrences"], "--max-occurrences"),
120
130
  description: values.description ?? "A mandate issued by metal-agent",
121
131
  out: need(values.out, "--out"),
122
- agentKeyPath: values["agent-key"],
132
+ mandateKeyFile: values["mandate-key-file"],
123
133
  // The keys that sign the owner's credentials are as private as the owner's key: they live beside it.
124
134
  issuerKeyPath: values["issuer-key"] ?? issuerKeyPath(need(values.key, "--key, the account owner's key file, beside which the keys that sign its credentials live, or --issuer-key")),
125
135
  }, print);
@@ -145,14 +155,12 @@ async function main(argv) {
145
155
  await mandateStatus(on(), need(values.credential, "--credential"), accountFactory(), print);
146
156
  }
147
157
  else if (sub === "pull") {
148
- const credentialPath = need(values.credential, "--credential");
149
- const agentKey = values["agent-key"] ?? agentKeyPath(credentialPath);
150
158
  const args = {
151
- credentialPath,
152
- agentKeyPath: agentKey,
159
+ credentialPath: need(values.credential, "--credential"),
160
+ mandateKeyFile: values["mandate-key-file"],
153
161
  to: parseAddress(values.to, "--to"),
154
162
  amount: parsePullAmount(values.amount, "--amount"),
155
- keyPath: values.key ?? gasKeyPath(agentKey),
163
+ keyPath: values.key,
156
164
  recordRefusal: values["record-refusal"] === true,
157
165
  };
158
166
  return (await mandatePull(on(), args, print)) === "paid" ? 0 : 1;
@@ -181,6 +189,14 @@ async function main(argv) {
181
189
  throw new Error(`unknown command "${command}"\n\n${helpFor([])}`);
182
190
  }
183
191
  }
192
+ /** How many words a command takes before its options: `key import <file|hex>` and `mandate credential create` three, `mcp` one, the other commands two; any number for `help` and for a command the dispatch then names unknown. */
193
+ function argumentCount(command, sub) {
194
+ if ((command === "key" && sub === "import") || (command === "mandate" && sub === "credential"))
195
+ return 3;
196
+ if (command === "mcp")
197
+ return 1;
198
+ return ["key", "fund", "pay", "account", "mandate", "status"].includes(command ?? "") ? 2 : Infinity;
199
+ }
184
200
  main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
185
201
  // An endpoint's URL can carry its credential, and this output often lands in an agent's transcript.
186
202
  console.error(`error: ${redactUrls(error instanceof Error ? error.message : String(error))}`);
package/dist/keystore.js CHANGED
@@ -14,16 +14,12 @@ function beside(path, suffix) {
14
14
  const ext = extname(path);
15
15
  return `${path.slice(0, path.length - ext.length)}.${suffix}${ext}`;
16
16
  }
17
- /** The P-256 key an agent signs payments with under the mandate at `credentialPath`: `mandate.json` to `mandate.agent-key.json`. */
18
- export const agentKeyPath = (credentialPath) => beside(credentialPath, "agent-key");
17
+ /** The mandate key `credential create` writes beside the credential at `credentialPath`: `mandate.json` to `mandate.key.json`. */
18
+ export const mandateKeyPath = (credentialPath) => beside(credentialPath, "key");
19
+ /** The directory of pulls `mandate pull` sent without an outcome yet, beside the credential at `credentialPath`: `mandate.json` to `mandate.pulls`. */
20
+ export const pendingPullsPath = (credentialPath) => `${credentialPath.slice(0, credentialPath.length - extname(credentialPath).length)}.pulls`;
19
21
  /** The keys that sign the owner's credentials, beside the owner's key file and as private as it: `owner-key.json` to `owner-key.issuer.json`. */
20
22
  export const issuerKeyPath = (ownerKeyPath) => beside(ownerKeyPath, "issuer");
21
- /** The funded key that submits the agent's direct pulls and pays their gas, beside its mandate key: `mandate.agent-key.json` to `mandate.gas-key.json`, `shared.json` to `shared.gas-key.json`. */
22
- export function gasKeyPath(agentKeyPath) {
23
- const ext = extname(agentKeyPath);
24
- const stem = agentKeyPath.slice(0, agentKeyPath.length - ext.length);
25
- return `${stem.endsWith(".agent-key") ? stem.slice(0, -".agent-key".length) : stem}.gas-key${ext}`;
26
- }
27
23
  /** The secp256k1 key at `path`, generated and written there first when the file does not exist. */
28
24
  export async function readOrCreateKey(path) {
29
25
  if (await exists(path))
package/dist/mcp.js CHANGED
@@ -7,19 +7,19 @@ import { fund } from "./commands/fund.js";
7
7
  import { parseMax, pay, payUnderMandate } from "./commands/pay.js";
8
8
  import { status } from "./commands/status.js";
9
9
  import { loadMandate, mandateAgentSigner } from "./commands/mandate.js";
10
- import { DEFAULT_KEY_PATH, agentKeyPath, readKey } from "./keystore.js";
10
+ import { DEFAULT_KEY_PATH, readKey } from "./keystore.js";
11
11
  import { USDZ, formatUsdz } from "@metalntwx/metal-pay";
12
12
  import { redactUrls } from "@metalntwx/metal-pay/mandates";
13
13
  /** The authority `metal-agent mcp` serves under, from its options. */
14
14
  export function mcpAuthority(options) {
15
15
  if (options.mandate !== undefined) {
16
16
  if (options.key !== undefined || options.max !== undefined) {
17
- throw new Error("mcp --mandate takes neither --key nor --max: the mandate sets the limit, and the agent's key is --agent-key");
17
+ throw new Error("mcp --mandate takes neither --key nor --max: the mandate sets the limit, and the agent signs with its mandate key");
18
18
  }
19
- return { kind: "mandate", credentialPath: options.mandate, agentKeyPath: options["agent-key"] ?? agentKeyPath(options.mandate) };
19
+ return { kind: "mandate", credentialPath: options.mandate, ...(options["mandate-key-file"] !== undefined && { mandateKeyFile: options["mandate-key-file"] }) };
20
20
  }
21
- if (options["agent-key"] !== undefined)
22
- throw new Error("--agent-key goes with --mandate");
21
+ if (options["mandate-key-file"] !== undefined)
22
+ throw new Error("--mandate-key-file goes with --mandate");
23
23
  return { kind: "key", keyPath: options.key ?? DEFAULT_KEY_PATH, maxAmount: parseMax(options.max, "--max") };
24
24
  }
25
25
  const usdz = (units) => `${formatUsdz(units)} ${USDZ.symbol}`;
@@ -43,7 +43,7 @@ export function createMcpServer(authority) {
43
43
  const BUY_RPC = "To buy Metal RPC, POST a JSON-RPC body to the RPC URL; the rpc-aas-endpoint header is the endpoint the payment bought, so send further RPC calls there without paying again.";
44
44
  if (authority.kind === "mandate") {
45
45
  server.registerTool("pay", {
46
- description: `Request a URL and pay its x402 challenge in *USDZ on Metal under this server's mandate: the payment comes from the mandate's account, signed by the agent's key, within the limits its owner set, and a payment outside them is refused with the limit named. Only the owner can change the limits. Returns the settlement transaction hash, the response headers and the body. ${BUY_RPC}`,
46
+ description: `Request a URL and pay its x402 challenge in *USDZ on Metal under this server's mandate: the payment comes from the mandate's account, signed by the mandate key, within the limits its owner set, and a payment outside them is refused with the limit named. Only the owner can change the limits. Returns the settlement transaction hash, the response headers and the body. ${BUY_RPC}`,
47
47
  // Strict: a key or limit the model names is refused, not dropped.
48
48
  inputSchema: z.object(request).strict(),
49
49
  }, ({ url, network: net, method, body, headers }) => collect((print) => payUnderMandate(target(net), authority, url, print, { method, body, headers })));
@@ -97,10 +97,10 @@ async function collect(run) {
97
97
  return { content: [{ type: "text", text: lines.join("\n") }], isError: true };
98
98
  }
99
99
  }
100
- /** Serves over stdio. A mandate and its agent key are checked first, so a wrong path fails the host's launch; a key is read when pay needs
100
+ /** Serves over stdio. A mandate and its mandate key are checked first, so a wrong path fails the host's launch; a key is read when pay needs
101
101
  * it, so fund and status work without one. */
102
102
  export async function serveMcp(authority) {
103
103
  if (authority.kind === "mandate")
104
- await mandateAgentSigner(await loadMandate(authority.credentialPath), authority.agentKeyPath);
104
+ await mandateAgentSigner(await loadMandate(authority.credentialPath), authority.credentialPath, authority.mandateKeyFile);
105
105
  await createMcpServer(authority).connect(new StdioServerTransport());
106
106
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@metalntwx/metal-agent",
3
- "version": "0.2.0",
4
- "description": "The `metal-agent` CLI and MCP server: create or import an agent key, fund it on Metal devnet or testnet, pay x402 offers in *USDZ, look up a settlement, and set spending limits for an agent that Metal enforces.",
3
+ "version": "0.3.0",
4
+ "description": "The `metal-agent` CLI and MCP server: create or import the agent's key, fund it on Metal devnet or testnet, pay x402 offers in *USDZ, look up a settlement, and set spending limits for an agent that Metal enforces.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -19,14 +19,14 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@metalntwx/metal-ts": "^0.3.2",
22
- "@metalntwx/metal-pay": "^0.2.0",
23
- "@modelcontextprotocol/sdk": "1.31.0",
24
- "@x402/core": "2.21.0",
22
+ "@metalntwx/metal-pay": "^0.3.0",
23
+ "@modelcontextprotocol/sdk": "1.32.1",
24
+ "@x402/core": "2.28.0",
25
25
  "viem": "^2.54.6",
26
26
  "zod": "^3.25.76"
27
27
  },
28
28
  "dependencyRationale": {
29
- "@metalntwx/metal-ts": "Funds the agent key with a MIP20 transfer, decodes settled transfers, and sends the EIP-7702 delegation a mandate needs.",
29
+ "@metalntwx/metal-ts": "Funds a key on devnet with a MIP20 transfer.",
30
30
  "@metalntwx/metal-pay": "The one-call x402 payment clients, mandates, and the Metal network constants.",
31
31
  "@modelcontextprotocol/sdk": "The stdio MCP server behind `metal-agent mcp`.",
32
32
  "@x402/core": "Decodes the `payment-response` header a paid request returns.",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: metal-agent
3
- description: "Set up and use the metal-agent CLI to pay x402 sellers on Metal: install it, create or import the agent's account, fund it, pay a URL that answers 402, pay under a mandate from an account the agent does not own, and look up a settlement. Use when a task needs an agent to pay for an HTTP resource in *USDZ on Metal, to pay with a mandate credential the user gives it, to set up metal-agent, or to check what a Metal payment settled."
3
+ description: "Set up and use the metal-agent CLI to pay x402 sellers on Metal: install it, create or import the agent's key, fund it, pay a URL that answers 402, pay under a mandate from an account the agent does not own, and look up a settlement. Use when a task needs an agent to pay for an HTTP resource in *USDZ on Metal, to pay with a mandate credential the user gives it, to set up metal-agent, or to check what a Metal payment settled."
4
4
  ---
5
5
 
6
6
  `metal-agent` is the CLI and MCP server of the `@metalntwx/metal-agent` npm
@@ -71,7 +71,7 @@ Run the steps in order. After each step, show the user its output.
71
71
  The tools appear after the host reloads. If you cannot reload it, tell
72
72
  the user to, and carry on with the CLI.
73
73
 
74
- 5. Get the agent's account. Ask the user whether they have a funded key file. If they do,
74
+ 5. Get the agent's key. Ask the user whether they have a funded key file. If they do,
75
75
  ask for its path and run `metal-agent key import <path>`. Otherwise run
76
76
  `metal-agent key create`. Tell the user the address. If `key create`
77
77
  answers that the key file already holds a key, keep that key and ask the
@@ -112,12 +112,12 @@ the user; the limits bind you only while it is not.
112
112
 
113
113
  ## key create
114
114
 
115
- Generate the agent's account and write its key file. Prints the address
115
+ Generate the agent's key and write its key file. Prints the address
116
116
  and the key file's path. It does not overwrite an existing key file; `--out` writes
117
117
  another.
118
118
 
119
119
  ```bash
120
- metal-agent key create --out ./agent-key.json
120
+ metal-agent key create --out ./key.json
121
121
  ```
122
122
 
123
123
  ## key import
@@ -149,7 +149,7 @@ named network, sign the authorization with the key file, retry, and print
149
149
  the settlement, the response headers and the body.
150
150
 
151
151
  ```bash
152
- metal-agent pay https://x402-demo-testnet.metalntwx.com/quote --network testnet --key ./agent-key.json
152
+ metal-agent pay https://x402-demo-testnet.metalntwx.com/quote --network testnet --key ./key.json
153
153
  ```
154
154
 
155
155
  Read `transaction <hash>` from the output for the settlement,
@@ -208,14 +208,16 @@ when the mandate names them, the payees.
208
208
  metal-agent pay https://example.com/report --mandate ./mandate.json --network testnet
209
209
  ```
210
210
 
211
- `--agent-key <file>` names your agent key when it is not the
212
- `mandate.agent-key.json` beside the credential; `--max` and `--key` do not
211
+ `pay` signs with the mandate key beside the credential, `mandate.key.json`
212
+ for `mandate.json`; `--mandate-key-file <file>` names it when it is
213
+ elsewhere. Never show the key file's contents. `--max` and `--key` do not
213
214
  apply. A payment outside the mandate exits 1 with
214
215
  `error: payment refused: <reason> (…)`, a reason starting `mandate_`. Tell
215
216
  the user which limit it was; only the owner can change a mandate. After
216
217
  `mandate_revoked`, ask the user for the credential that replaced it, and
217
- pay with that. If the error says there is no agent key, pass the
218
- `--agent-key` it names.
218
+ pay with that. If the error says no key beside the credential is this
219
+ mandate's, ask the user where the key file is, and pass it as
220
+ `--mandate-key-file`.
219
221
 
220
222
  A seller that asks for your signed order gets it with the payment, and
221
223
  `pay` prints it as `signed-offer <text>`. Keep it, and pass it to
@@ -229,13 +231,18 @@ together.
229
231
  metal-agent mandate status --credential ./mandate.json --network testnet --rpc-url <endpoint>
230
232
  ```
231
233
 
232
- `mandate pull` pays an address the mandate allows, with no seller. The gas
233
- key beside the credential, `mandate.gas-key.json`, submits it and pays the
234
- gas, so ask the user to fund its `gas-key-address` first. `outcome paid`, or `outcome refused, not sent` with `reason` and `recorded no` lines,
235
- says what happened; a pull refused when checked sends nothing and costs no gas. Pass
236
- `--record-refusal` only when the user asks for the refusal to be recorded
237
- on chain. If a pull fails after printing `transaction <hash>`, run
238
- `metal-agent status <hash>` before pulling again; it may have paid.
234
+ `mandate pull` pays an address the mandate allows, with no seller. The
235
+ facilitator submits it and pays the gas. The `outcome` line says what
236
+ happened: `paid`; `refused, not sent` with `reason` and `recorded no`, for
237
+ a pull refused when checked, which sends nothing; `refused` with `reason`,
238
+ for one the account refused and recorded; `refused, not paid`, for a sent
239
+ pull that can no longer land; `held`, for one the payee's
240
+ receive policy sent to escrow, so the payee was not paid; or `pending`. Pass
241
+ `--record-refusal`, with `--key` naming a funded key to send it, only when
242
+ the user asks for the refusal to be recorded on chain. On `outcome pending`,
243
+ run the same command again, with the same `--to` and `--amount`: it sends
244
+ the same pull, which pays at most once, or reports how it ended. Never
245
+ change the amount to pay a pending pull again.
239
246
 
240
247
  ```bash
241
248
  metal-agent mandate pull --credential ./mandate.json --to <payee address> --amount 2.50 --network testnet --rpc-url <endpoint>