Skip to main content
metal-agent is the CLI and MCP server of the @metalntwx/metal-agent npm package. It pays x402 sellers in *USDZ on Metal: an agent requests a URL, the seller answers 402 with a price, and metal-agent signs the payment and returns the resource with the settlement. Every command that touches the chain takes --network testnet. Metal’s demo seller is at https://x402-demo-testnet.metalntwx.com/quote and the block explorer at https://testnet.metalntwx.com. key, fund and pay need no RPC endpoint. status, account create and the mandate commands other than credential create need the user’s own: ask for it, unless METAL_RPC_URL is already set, and pass it with --rpc-url. A user without one creates it as https://docs.metalntwx.com/network/endpoints describes. The key file defaults to ~/.metal-agent/key.json. It holds the private key in the clear, owner-readable only; treat the file as the secret. If metal-agent --version prints 0.3.3 and the user already has a key, go to the command you need. Otherwise start with Setup.

Setup

Run the steps in order. After each step, show the user its output.
  1. Check Node.js with node --version. If it is missing, tell the user to install Node.js, and stop.
  2. Install the CLI, or move an installed one to this version:
    Then metal-agent --version prints 0.3.3. If it prints anything else, another copy comes first on the PATH: tell the user, and stop.
  3. Install this skill, so that later sessions have it without fetching this page. Copy the directory $(npm root -g)/@metalntwx/metal-agent/skills/metal-agent into your skills directory. In Claude Code that is .claude/skills/metal-agent in the project, or ~/.claude/skills/metal-agent for every project. If your agent has no skills directory, skip this step.
  4. Connect the MCP server, which serves pay, fund and status as tools. In Claude Code:
    In any other MCP host, add this server to its MCP configuration:
    The server pays with the default key file. If the user’s key is in another file, or the agent pays under a mandate, add --key <file> or --mandate <credential> after mcp. Under a mandate on testnet, also set the user’s endpoint in the server’s environment, or in an env block in another host’s configuration:
    The tools appear after the host reloads. If you cannot reload it, tell the user to, and carry on with the CLI.
  5. Get the agent’s key. Ask the user whether they have a funded key file. If they do, ask for its path and run metal-agent key import <path>. Otherwise run metal-agent key create. Tell the user the address. If either answers that the key file already holds a key, tell the user the address in its address field, and ask whether to pay with that key.
  6. Fund the key from the testnet faucet, unless the user said it is funded, and show the user the transaction and explorer lines:
    If the faucet refuses, show the user its error and what it says to do.
  7. Pay Metal’s demo seller once and show the user the transaction and explorer lines:
  8. Tell the user what is set up: the CLI, the skill and where you put it, the MCP server, the key’s address and file, and the payment’s explorer link.

Stop and ask the user first

  • Before you pay a URL the user did not name. The demo seller in Setup is the one exception.
  • Before you pass a --max above the default of 1 *USDZ.
  • Before you import a key, or write a key anywhere but the default file.
  • Before you run account create, mandate credential create, bind, replace, renew or revoke. They are the account owner’s to run.
Never print a private key or the contents of a key file. Never move, copy or delete a key file. If the owner’s key file is within your reach, tell the user; the limits bind you only while it is not.

key create

Generate the agent’s key and write its key file. Prints the address and the key file’s path. It does not overwrite an existing key file; --out writes another.

key import

Load a key someone funded for you. Takes a hex key (with or without 0x), a file holding one, or a key file. Prints the address, never the key.

fund

Ask the public faucet for *USDZ; the faucet sets the amount. An address that already holds a full grant is reported as funded. A grant prints transaction <hash> and explorer <url>, the transaction’s page on the block explorer. The faucet limits how often one client can ask: on a client_rate_limited error, wait the seconds it names; on client_daily_limit_reached, fund the address from the Faucet page in the Metal console the error names; on either daily-limit error, load a pre-funded key with key import.

pay

Request a URL. If it answers 402 with an exact offer in *USDZ on the named network, sign the authorization with the key file, retry, and print the settlement, the response headers and the body.
Read transaction <hash> from the output for the settlement, header <name>: <value> lines for the response headers, and body … for the resource. explorer <url> is the settlement’s page on the block explorer; hand it to the user. pay refuses any offer above --max before it signs. The limit is 1 *USDZ per payment by default. Pass a larger --max only when the user agreed to that price.
For a request other than GET, pass --method (POST when there is a body), --body (sent as application/json unless a --header names a content type) and repeated --header 'name: value'; pay sends them again with the payment. To buy RPC over x402, POST the JSON-RPC call to https://rpc-testnet.metalntwx.com/v1:
The output’s header rpc-aas-endpoint: <url> is the endpoint that payment bought. Send further JSON-RPC calls to that URL with a plain HTTP POST, with no payment, until it answers 402 again; do not pay at the RPC URL again for each call. A refusal exits 1 with the reason:
  • error: payment refused before signing: offer of … is above the … limit per payment; pass --max to allow it means the offer is above the limit.
  • error: payment refused before signing: offer is in …, not *USDZ … or offer signs for the EIP-712 domain … means the offer asks for something other than *USDZ. Tell the user; a larger --max does not change it.
  • error: payment refused: <reason> means the facilitator refused at verification, or the seller’s own check refused; nothing was paid.
  • error: payment refused at settlement: <reason> means the facilitator refused at settlement for a reason that moves nothing, with the transaction hash when there is one; nothing was paid. On daily_limit_reached, retry later.
  • On error: payment in doubt at settlement: …, or any error after the nonce line that is not a payment refused line, such as fetch failed or an HTTP status from the seller, the payment may have settled. Do not pay again. Tell the user, with the nonce line pay printed; when the error names a transaction hash, status on it shows whether it landed.

pay --mandate

When the user gives you a mandate’s credential file, pay with it in place of a key. The payment comes from the owner’s account, within limits the owner set and Metal enforces: the most per payment, a total budget and, when the mandate names them, the payees.
pay signs with the mandate key beside the credential, mandate.key.json for mandate.json; --mandate-key-file <file> names it when it is elsewhere. Never show the key file’s contents. --max and --key do not apply. A payment outside the mandate exits 1 with error: payment refused: <reason> (…), a reason starting mandate_. Tell the user which limit it was; only the owner can change a mandate. After mandate_revoked, ask the user for the credential that replaced it, and pay with that. If the error says no key beside the credential is this mandate’s, ask the user where the key file is, and pass it as --mandate-key-file. A seller that asks for your signed order gets it with the payment, and pay prints it as signed-offer <text>. Keep it, and pass it to mandate verify --credential <file> --issuer <file> --tx <hash> --signed-offer <text> --network testnet --rpc-url <endpoint> to check the two together. mandate status shows what is left: read spent <amount> of <budget>, revoked yes|no and the payee lines.
mandate pull pays an address the mandate allows, with no seller. The facilitator submits it and pays the gas. Pass --payment-id with the id of the payment it is for, such as the refund’s or the invoice’s: two payouts of one amount to one payee, while the first is pending, are two payments only under two ids. The outcome line says what happened: paid, exit 0; refused, not sent with reason and recorded no, for a pull refused when checked, which sends nothing; refused with reason, for one the account refused and recorded; refused, not paid, for a sent pull that can no longer land; held, exit 4, for one the receive policy contract holds in escrow, so the payee was not paid and the amount counts against the budget: do not send it again; the payee, or the authority its policy names, releases it; or pending, exit 3. Refusals exit 1. Pass --record-refusal, with --key naming a funded key to send it, only when the user asks for the refusal to be recorded on chain. On outcome pending, run the same command again, on the same machine, with the same credential, --payment-id, --to and --amount: it sends the same pull, which pays at most once, or reports its outcome. Never add, drop or change the payment id, or change the amount, to pay a pending pull again. After outcome refused, nothing was paid, and the same payment id may be sent with other values. error: the pull was not sent: … names why, such as the facilitator’s refusal; tell the user. An error saying the pull kept before may have been sent and can land until … means that pull can still land until then: tell the user, and never pay it under another payment id. The same holds for an error naming an event the SDK does not decode: the pull may have paid.
mandate verify checks the mandate and every payment under it from the chain alone, or one --tx <hash>, and exits 1 when a check fails. Run it when the user asks whether a payment was within the mandate; read verified yes|no, and a failed step line names what differs. A failure that names a topic the SDK does not decode means metal-agent and the account may be different versions; tell the user. It needs the issuer’s public key file, the issuer-public-file that credential create printed; ask the user for it.

status

Show a settlement from chain data: status, block, submitter, gas paid in *USDZ, and the transfer it carried. Pass the endpoint the user gave you; without one, status exits with error: testnet needs an RPC endpoint of your own: sign up at https://console-testnet.metalntwx.com, create an endpoint, then pass --rpc-url or set METAL_RPC_URL.

mcp

Serve pay, fund and status as MCP tools, each taking a network, over stdio for an MCP host; under a mandate, mandate_pull, mandate_status and mandate_verify as well. What pay and mandate_pull spend is set when the server starts, by whoever starts it: a key file and --max, or one mandate’s credential file, which each call reads. pay takes the request as optional method, body and headers, and under a key a max that can only lower the server’s limit, and ends a refusal after signing, or a payment that may have settled, with a next: line. fund and status take the endpoint as rpcUrl, which status on testnet needs unless the server’s environment sets METAL_RPC_URL. With the server’s METAL_RPC_URL, fund asks the testnet faucet nothing for an address that already holds about a full grant. The mandate tools take no rpcUrl: they read the chain through the server’s METAL_RPC_URL, or devnet’s default endpoint, and fail naming a chain other than the network’s. mandate_pull takes to, amount and a paymentId, one per payment, and ends any outcome but paid with a next: line; mandate_verify takes the issuers to trust. No tool chooses a key, a mandate or a higher limit, and none sets or changes a mandate.