Privacy and safety

What the Solana chain can and cannot see when you use H081, where your key lives, the limits that protect you, and how retries stay safe.

In plain words

Most of what happens on a blockchain is public: anyone can look up who sent how much to whom. H081 is a bank account where your agent's balance, and its payments inside the bank, are private. Only your agent's key can read them.

Some steps still touch the public chain, like putting money in. This page shows exactly what is public and what is not, the one habit that keeps you private (withdraw to a fresh wallet), where your key lives, and the safety limits you can set.

What the public chain sees

Think of the bank as a closed room. Walking in and walking out happen on the street, where anyone can watch. What happens inside the room is hidden.

What you doWho sends itAnyone can seeStays private
Deposit (wallet → bank)Your agent's keyYour agent wallet and the amountWhat you do with the money afterwards
Withdraw through the relayer (the default)The H081 relayerThe wallet that receives, and the amount it receivesYour agent wallet: it is not shown as the sender
Withdraw with --directYour agent's keyYour agent wallet as the sender, the receiving wallet and the amountYour remaining balance
Transfer or payment to an h081-address: code (another H081 account)Your agent's keyThat your agent wallet used the bankTo whom, and how much
Payment from a budget to a normal walletThe H081 relayerThe wallet that receives, and the amountWhich account paid
Give a budget (allowance)Your agent's keyThat your agent wallet made a grantThe amount, the rules and who got it
LendYour agent's keyYour agent wallet as the lender, the amount lent and the borrower's payout accountThe borrower's budget and payments

Relayer: a server run by H081 at https://api.h081.xyz that sends withdrawals for you. It receives a finished proof, never your key, and charges 0.05 USDG or 0.20%, whichever is more.

Example conversation · wording variesAsking Claude what is private
  1. You

    Can anyone see how much is in my H081 account?

  2. Claude

    uses h081_bank_balance

    No. Your private balance is 3 USDG, and only your agent's key can read it.

    What is public: each deposit (your agent wallet and the amount) and each withdrawal (the receiving wallet and the amount). A withdrawal through the relayer does not show your agent wallet as the sender.

The fresh-wallet rule

Withdraw to a wallet that has never touched your agent. If money goes in from one wallet and comes straight back out to the same wallet, anyone can connect the two, and the privacy is gone.

The mistake, step by step

  1. Your personal wallet 7Hk… sends 10 USDG to your agent wallet. Public.
  2. Your agent deposits 10 USDG into the bank. Public: agent wallet, 10 USDG.
  3. Five minutes later you withdraw 10 USDG to 7Hk…. Public: 7Hk… received 9.95 USDG from the relayer.

Anyone watching sees 10 USDG leave 7Hk… and almost 10 USDG come back to 7Hk… minutes later. The bank hid nothing.

The same goes for --to self: "self" is your agent wallet, which is the wallet that deposited.

Doing it right

  1. Make a new wallet

    In your wallet app, create a new account. It must never have sent to, or received from, your agent wallet or your personal wallet.

  2. Wait a while

    Do not withdraw right after depositing. The more time passes, and the more other people use the bank in between, the harder it is to match your withdrawal to your deposit.

  3. Check the fee, then see the plan

    Without --yes nothing is sent. The plan shows the fee, what arrives and what is public.

    fee quote (nothing is sent)
    npx -y @h081lll/agent fee 10
    the plan (nothing is sent)
    npx -y @h081lll/agent withdraw 10 --to <a wallet that never touched the agent>
    response (shortened)
    { "ok": true, "network": "mainnet-beta", "status": "plan",
      "result": { "op": "withdraw", "usdg": "10", "to": "<fresh wallet>", "fee": "0.05", "payout": "9.95",
                  "publicOnChain": ["recipient", "amount received"], "sentBy": "relayer",
                  "warnings": ["If <fresh wallet> has no USDG account yet, the relayer adds 1 USDG to create it."],
                  "next": "Nothing was sent. ..." },
      "requestId": "..." }
  4. Send it

    send
    npx -y @h081lll/agent withdraw 10 --to <a wallet that never touched the agent> --yes

    A fresh wallet often has no USDG account yet. Then the relayer takes 1 USDG more to create one; the plan says so under warnings.

The same withdrawal from code, plan first:

macOS / Linux (bash)
TOKEN=$(cat ~/.h081/api-token)
# 1 · the plan: no "confirm", so nothing is sent
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"usdg":"10","to":"<fresh wallet>"}' http://127.0.0.1:8081/v1/withdraw
# 2 · send it, with a key that makes a retry safe
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: wd-fresh-1" \
  -d '{"usdg":"10","to":"<fresh wallet>","confirm":true}' http://127.0.0.1:8081/v1/withdraw
Windows PowerShell
$h = @{ Authorization = "Bearer $(Get-Content $HOME\.h081\api-token)" }
# 1 · the plan: no "confirm", so nothing is sent
Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body '{"usdg":"10","to":"<fresh wallet>"}' http://127.0.0.1:8081/v1/withdraw
# 2 · send it, with a key that makes a retry safe
$h["Idempotency-Key"] = "wd-fresh-1"
Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body '{"usdg":"10","to":"<fresh wallet>","confirm":true}' http://127.0.0.1:8081/v1/withdraw
Python (requests)
import pathlib, uuid, requests

API = "http://127.0.0.1:8081/v1"
s = requests.Session()
s.headers["Authorization"] = "Bearer " + (pathlib.Path.home() / ".h081" / "api-token").read_text().strip()

body = {"usdg": "10", "to": "<fresh wallet>"}
print(s.post(API + "/withdraw", json=body).json()["result"])      # the plan: nothing is sent

key = str(uuid.uuid4())                                            # keep it: a retry with it never sends twice
r = s.post(API + "/withdraw", json={**body, "confirm": True}, headers={"Idempotency-Key": key}).json()
print(r)
TypeScript
import { H081 } from '@h081lll/agent';

const bank = await H081.open();
const to = '<fresh wallet>';
console.log(await bank.withdraw('10', { to }));                       // the plan: nothing is sent
console.log(await bank.withdraw('10', { to, confirm: true, idempotencyKey: 'wd-fresh-1' }));
Example conversation · wording variesWithdrawing with Claude
  1. You

    Withdraw 10 USDG to my fresh wallet 9xQe…. It has never received anything from this agent.

  2. Claude

    uses h081_bank_withdraw

    Done. 9.95 USDG is on its way to 9xQe…; the relayer fee was 0.05 USDG.

    The relayer sent it, so your agent wallet does not show up as the sender.

Withdrawal and deposit limits

These limits are set by the H081 programs and the relayer, not by you.

LimitAmountIf you go over it
Withdrawal through the relayer, nothing stakedUp to 100 USDG per withdrawalError needs_stake. Split it into smaller withdrawals, or stake H081.
Withdrawal through the relayer, H081 stakedH081 staked in capacity_stake sets a 30-day limitSee your stake with stake view below
Withdrawal with --directNo relayer limit and no feeYour agent wallet is public as the sender
One depositAt most 1,000 USDGError cap_per_deposit
The whole bank10,000 USDG in total, for nowError cap_total
see your stake
npx -y @h081lll/agent stake view

Unstaking has a 30-day cooldown: you request it first, then unstake once 30 days have passed.

over 100 USDG with nothing staked: the plan for a direct withdrawal (nothing is sent; your wallet would be public)
npx -y @h081lll/agent withdraw 150 --to <a wallet that never touched the agent> --direct

Your key stays on your computer

Your agent has one key. It pays network fees, holds the public USDG going in and out, and opens your private account. Whoever has this key has the money. H081 never has it: it is made on your computer and never sent anywhere.

Where things are kept

File in ~/.h081Who can read itWhat it is
agent.jsonOnly you (0600)The agent key
config.jsonOnly you (0600)Network, RPC URL and port, written by init
api-tokenOnly you (0600)The password for the local API
ops.jsonlOnly you (0600)Your local history and retry records (never a key, a note or a token)
cache/, circuits/Public chain data and the proof files

On macOS and Linux the folder itself is 0700 (only you). On Windows, access is limited to your user with icacls; if that fails you see a warning, and you should check the folder's permissions yourself. H081_HOME moves the folder somewhere else.

where your key comes from, your RPC host, the SOL each use needs
npx -y @h081lll/agent info

Back up the key

If your computer dies, the key file dies with it, and so does the money. Make a backup code now. It is the key written as letters and digits, with a checksum and the network built in, so a typo is caught.

print the backup code (command line only)
npx -y @h081lll/agent backup
restore on a new computer
npx -y @h081lll/agent restore <your h081-key: code>

Restore checks the code first. It refuses to replace a key that is already there unless you add --force.

Spending ceilings

A ceiling is a hard upper limit you set on your own computer. Anything above it is refused before a proof is made or anything is sent, whoever asks: you, a script or Claude.

SettingLimits
H081_MAX_DEPOSITEach deposit
H081_MAX_PAYEach payment from a budget
H081_MAX_GRANTEach budget you give
H081_MAX_LENDEach loan you fund

Values are in base units: 1 USDG = 1000000. None is set by default. There is no ceiling setting for withdrawals; for those, the small balance on a dedicated key is your limit.

macOS / Linux (bash)
# at most 20 USDG per payment and 50 USDG per deposit (base units: 1 USDG = 1000000)
export H081_MAX_PAY=20000000
export H081_MAX_DEPOSIT=50000000
npx -y @h081lll/agent serve
Windows PowerShell
# at most 20 USDG per payment and 50 USDG per deposit (base units: 1 USDG = 1000000)
$env:H081_MAX_PAY = "20000000"
$env:H081_MAX_DEPOSIT = "50000000"
npx -y @h081lll/agent serve
for Claude Code: ceilings on the MCP server
claude mcp add --scope user h081 -e H081_SOLANA_KEYPAIR=<absolute path to ~/.h081/agent.json> -e H081_RPC_URL=<your RPC> -e H081_MAX_PAY=20000000 -e H081_MAX_GRANT=20000000 -- npx -y @h081lll/agent mcp

On Windows (not WSL), the end of that line is -- cmd /c npx -y @h081lll/agent mcp.

what an over-the-ceiling payment returns
{ "ok": false, "network": "mainnet-beta",
  "error": { "code": "over_operator_ceiling",
             "message": "over the operator ceiling H081_MAX_PAY (20 USDG per payment; asked 25 USDG). Nothing was sent.",
             "retryable": false },
  "requestId": "..." }

How the local API protects you

The local API (npx -y @h081lll/agent serve) is a small server on your own computer. It is built so that only programs you run can use it.

  • Your computer only. It listens on 127.0.0.1. There is no option to open it to the network.
  • A password on every call. Every route except /v1/health and /v1/openapi.json needs Authorization: Bearer <token>, with the token from ~/.h081/api-token. The check takes the same time for right and wrong tokens, so it cannot be guessed by timing.
  • Web pages are refused. A request from a browser page (one with an Origin header) is refused unless you started the server with --allow-origin <that exact URL>. The Host header must be 127.0.0.1:<port> or localhost:<port>, which blocks websites that try to reach your computer through a trick address (DNS rebinding).
  • JSON only. A POST must be Content-Type: application/json.
  • Plan first. A write without "confirm": true only returns the plan. Nothing is proven, signed or sent.
  • Speed limits. At most 30 sends a minute (H081_API_MAX_SENDS_PER_MIN), one proof at a time, and at most 8 waiting.
  • Quiet logs. Keys, tokens, notes and full RPC URLs are never logged; only the RPC host is shown.
  • Checked downloads. The proof files come from h081.xyz and are checked against pinned sha256 fingerprints before use.

If you think the token leaked, replace it, then restart serve. Programs using the old one get 401 unauthorized until you give them the new one.

new API token
npx -y @h081lll/agent token --rotate

Retries are safe

Networks fail. If a payment seems to fail, the natural thing is to try again. The danger is paying twice. H081 prevents that with an idempotency key: a label you attach to one send, like a ticket number.

  • Send again with the same label and you get the first answer back. It is never sent twice.
  • A new payment gets a new label.

In the API it is the Idempotency-Key header (1 to 128 letters, digits and _ . : -), and a send with "confirm": true must have one. In the SDK it is idempotencyKey. The command line makes a new random one on every run unless you pass --idempotency-key:

a command line send you can safely run again
npx -y @h081lll/agent withdraw 10 --to <a wallet that never touched the agent> --yes --idempotency-key wd-fresh-1
You send the same key again, and…You get
the first one finishedThe stored answer again, with "replayed": true
the first one is still running409 in_progress; wait 5 seconds (Retry-After)
the body is different422 idempotency_key_reused; use a new key
the first one failed with nothing sent (relayer busy, 429, stale root)It runs again: the key was released
the first one failed with an error that is not retryable (for example needs_sol)The same error again. Fix the cause, then send with a new key
no one can tell whether the first one went through409 outcome_unknown, every time; it is never sent again

Retry records are kept in ~/.h081/ops.jsonl for 7 days.

When the outcome is unknown

This happens when the local server stopped in the middle of a send, or when the connection to the relayer or to the Solana RPC broke while it was sending. The money may already be on its way.

  1. Do not send again yet

    Retrying with the same key keeps answering outcome_unknown. That is on purpose.

    response
    { "ok": false, "network": "mainnet-beta",
      "error": { "code": "outcome_unknown",
                 "message": "the gateway stopped while the request with Idempotency-Key wd-fresh-1 was running, so its outcome is unknown: check /v1/balance before retrying with a new key",
                 "retryable": false, "details": { "idempotencyKey": "wd-fresh-1" } },
      "requestId": "..." }
  2. Check the balance and the history

    --fresh reads the chain now instead of the 15-second cache.

    any terminal
    npx -y @h081lll/agent balance --fresh
    npx -y @h081lll/agent history --limit 5

    From the API: GET /v1/balance?fresh=1 and GET /v1/history.

  3. Only if it did not go through, send again with a new key

    If the balance did not drop and nothing arrived, it was not sent. Send it again with a new idempotency key.

Safety checklist

  • Use a dedicated agent key that holds only a few dollars.
  • Back up the key with backup and keep the code offline.
  • Withdraw to a fresh wallet, and wait a while after depositing.
  • Read the plan before adding --yes or "confirm": true.
  • Set H081_MAX_PAY and H081_MAX_GRANT before letting an AI spend.
  • Give every new payment a new idempotency key; reuse a key only to retry that same payment.
  • On outcome_unknown, check the balance before anything else.

New here? The 5-minute quickstart does a first private payment end to end. The package is @h081lll/agent on npm. Questions: @H081CAP on X.