Pay an agent that has no account.

A transfer needs somewhere to send it. A smail does not: it seals ETH against a key that exists only in a link, and whoever holds the link takes the contents. There is nothing to register, no address to ask for, and, because the proof is made in JavaScript, nothing that needs a browser or a wallet extension. A private key in a terminal can do every part of it, the private links included.

Install

Terminal
npm install -g smail-cli

Nothing works until this exists. Create a .env.local in your project with two variables in it:

.env.local
SMAIL_PRIVATE_KEY=0x…            # the key that signs and pays gas
SMAIL_NETWORK=robinhood-testnet   # or robinhood, or anvil

No export, no quotes needed, though both are tolerated if you paste them. Exported shell variables always win over a file, so a container that injects a key cannot have it replaced by a dotfile in a checkout. Whatever holds that key holds the money: keep the file out of git. Three more are optional; the last only matters when whoami says the prover’s artifacts were not found, which opening a private link needs:

.env.local, optional
SMAIL_RPC=https://…               # a node of your own, optional
SMAIL_RELAYER_URL=https://…       # another relayer, or none; the network's own is the default
SMAIL_ZK_DIR=/path/to/zk          # the prover's artifacts, if the package's copy is not found
Terminal
smail whoami

Prints the address, its balance, the network, the contracts, whether the contract is sponsored, whether a relayer is configured and which file it read, so an agent is never signing with a key it cannot account for.

Send

Terminal
smail seal --amount 0.01 --expiry 24h --memo "invoice 1101"
In a terminal
Sealed 0.01 ETH on Robinhood Chain testnet, claimable for 1 day, paying 0.0101 ETH: the amount and a 0.0001 ETH fee that has a relayer deliver it for nothing.
Funded from 0xf39F…2266: the chain sees this address pay. --private seals a private
link instead, whose claim nothing on chain ties back to you.

Claim link   https://smail.cash/claim#s1.BSc7nv0…
Return link  https://smail.cash/refund#r1.B4jwdCU…~0x7e43…

Anyone holding the claim link can take the contents, so send it the way you
would send cash. Keep the return link: after the window shuts it is the only
way to get the money back.

Transaction  https://explorer.testnet.chain.robinhood.com/tx/0x2a927023701c…
Piped anywhere else
{
  "ok": true,
  "network": "robinhood-testnet",
  "amount": "0.01",
  "token": "ETH",
  "fee": "0",
  "deliveryFee": "0.0001",
  "total": "0.0101",
  "relayer": null,
  "memo": "invoice 1101",
  "unlocksAt": null,
  "expiresAt": "2026-09-27T07:41:48.000Z",
  "claimLink": "https://smail.cash/claim#s1.BSc7nv0…",
  "returnLink": "https://smail.cash/refund#r1.B4jwdCU…~0x7e43…",
  "claimKey": "0x7e43138dca13d7ab1cfd1e892bdcdcf8d49258ea",
  "envelopeId": "0x7e43138dca13d7ab1cfd1e892bdcdcf8d49258ea",
  "locked": false,
  "private": false,
  "sponsored": true,
  "funder": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "transactionHash": "0x2a927023701c734c6a91…",
  "explorer": "https://explorer.testnet.chain.robinhood.com/tx/0x2a927023701c…"
}

One command, two shapes. Read by a person it is prose; read by anything else it is JSON, decided by whether stdout is a terminal rather than by remembering a flag. The chain sees this address fund the smail. On a sponsored deployment the key pays 1% on top, which has a relayer deliver the claim and the refund for nothing, so the recipient gets the whole amount and needs no ETH. --password locks the link so the recipient needs both halves, --unlock 1h refuses claims for the first hour, and --expiry never makes a smail that can never be taken back. Anyone holding the claim link can take the contents, so pass it the way you would pass cash. Keep the return link: after the window shuts it is the only way to get the money back.

Receive

Terminal
smail open "https://smail.cash/claim#s1.BSc7nv0…"
Terminal
smail open "https://smail.cash/claim#s1.BSc7nv0…" --to 0x…
Terminal
smail status "https://smail.cash/claim#s1.BSc7nv0…"

No flag pays the key’s own address; --to pays another. With a relayer configured the claim key in the link signs, the relayer submits, and the opener needs no ETH at all. A smail releases exactly once, which is what makes a link safe to send over a channel you do not control. status reads the contract and every transaction that touched the smail, and takes a claim link, a return link, a private link or, with --id, the claim key’s address.

Take it back

Terminal
smail refund "https://smail.cash/refund#r1.B4jwdCU…~0x7e43…"

Only after the claim window has shut, and only with the return link. --to sends it somewhere else, except on a sponsored deployment, where a smail only ever goes back to the address that funded it, whoever runs this.

Private links

Terminal
smail seal --private --amount 0.01
In a terminal
Sealed a private 0.01 ETH link on Robinhood Chain testnet, paying 0.0101 ETH.
The chain sees 0xf39F…2266 seal 0.01 ETH into the pool. The claim will show only
a recipient, and nothing on chain pairs the two.

Private link  https://smail.cash/claim#p1.9kQe2Lw…

Anyone holding the link can take the contents, so send it the way you would send
cash. There is no return link and no expiry: keep a copy, and smail open <link>
takes it back yourself for as long as nobody else has.
Terminal
smail open "https://smail.cash/claim#p1.9kQe2Lw…"

A p1. link carries two random secrets instead of a key, and is sealed into a pool of fixed sizes: 0.001, 0.005, 0.01, 0.05 and 0.1 ETH. Sealing costs the amount plus 1%, prepaid for whoever relays the claim; open it from your own key and the 1% comes back with the amount. Opening reads the pool’s whole deposit log and proves the claim on this machine, which takes a few seconds. No window, no memo, no password and no return link: the sender keeps a copy of the same link and takes the money back with smail open, for as long as nobody else has.

In a pipeline

Terminal
LINK=$(smail seal --amount 0.01 | jq -r .claimLink)
curl -X POST "$WEBHOOK" -d "{\"pay\": \"$LINK\"}"
Terminal
smail status "$LINK" | jq -e '.status == "claimed"' && echo paid
Rehearse
smail seal --amount 0.01 --dry-run

Every command returns ok, and a failure carries error on stderr with exit code 1, so a caller branches on fields rather than sentences. --dry-run builds and prints the transaction, proof included for a private link, without signing or sending it. Worth doing once before wiring this into anything that spends on its own.

Or the library

Terminal
npm install smail-sdk

The CLI is a thin wrapper around smail-sdk, the same package this web app is built on: keys, links, locks, the release signatures, the private-link tree and prover, and the contract calls. Its README carries the API.

What a key alone can do

All of it. Seal, open, take an expired smail back, and seal and open a private link. The proof that claims a private link is a Groth16 proof made by the SDK, in JavaScript, from the link and the pool’s deposit log, so there is no wallet to ask and nothing a terminal cannot do that a browser can.

What stays visible
Amounts, always. Every part of a claim link’s sealing: the funder, the amount, the claim key, the window and the memo. And the address that submits a transaction: with SMAIL_RELAYER_URL set, a relayer submits, so your key is never the sender. Without one, your key submits and pays the gas.
What a private link hides
Only the pairing. The sealing shows the funder, the size and a commitment; the claim shows the recipient, the size and the relayer. Nothing on chain ties the two, so a claim hides among every unclaimed private link of the same size, which is little in a small pool. The sender can always see when and where it was claimed.

smail whoami prints whether a relayer is configured next to the key in use, so a caller can check rather than assume.