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.-
Check Node.js with
node --version. If it is missing, tell the user to install Node.js, and stop. -
Install the CLI, or move an installed one to this version:
Then
metal-agent --versionprints0.3.3. If it prints anything else, another copy comes first on thePATH: tell the user, and stop. -
Install this skill, so that later sessions have it without fetching this
page. Copy the directory
$(npm root -g)/@metalntwx/metal-agent/skills/metal-agentinto your skills directory. In Claude Code that is.claude/skills/metal-agentin the project, or~/.claude/skills/metal-agentfor every project. If your agent has no skills directory, skip this step. -
Connect the MCP server, which serves
pay,fundandstatusas 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>aftermcp. Under a mandate on testnet, also set the user’s endpoint in the server’s environment, or in anenvblock 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. -
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 runmetal-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 itsaddressfield, and ask whether to pay with that key. -
Fund the key from the testnet faucet, unless the user said it is funded,
and show the user the
transactionandexplorerlines:If the faucet refuses, show the user its error and what it says to do. -
Pay Metal’s demo seller once and show the user the
transactionandexplorerlines: - 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
--maxabove 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,reneworrevoke. They are the account owner’s to run.
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 without0x),
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 anexact 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.
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.
--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:
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 itmeans the offer is above the limit.error: payment refused before signing: offer is in …, not *USDZ …oroffer signs for the EIP-712 domain …means the offer asks for something other than*USDZ. Tell the user; a larger--maxdoes 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. Ondaily_limit_reached, retry later.- On
error: payment in doubt at settlement: …, or any error after thenonceline that is not apayment refusedline, such asfetch failedor an HTTP status from the seller, the payment may have settled. Do not pay again. Tell the user, with thenoncelinepayprinted; when the error names a transaction hash,statuson 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
Servepay, 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.