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
npm install -g smail-cliNothing works until this exists. Create a .env.local in your project with two variables in it:
SMAIL_PRIVATE_KEY=0x… # the key that signs and pays gas
SMAIL_NETWORK=robinhood-testnet # or robinhood, or anvilNo 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:
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 foundsmail whoamiPrints 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
smail seal --amount 0.01 --expiry 24h --memo "invoice 1101"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…{
"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
smail open "https://smail.cash/claim#s1.BSc7nv0…"smail open "https://smail.cash/claim#s1.BSc7nv0…" --to 0x…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
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
smail seal --private --amount 0.01Sealed 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.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
LINK=$(smail seal --amount 0.01 | jq -r .claimLink)
curl -X POST "$WEBHOOK" -d "{\"pay\": \"$LINK\"}"smail status "$LINK" | jq -e '.status == "claimed"' && echo paidsmail seal --amount 0.01 --dry-runEvery 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
npm install smail-sdkThe 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_URLset, 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.