Errors and fixes

Every H081 error code: what it means in plain words, whether trying again helps, and exactly what to do.

In plain words

When H081 cannot do something, it tells you why with a short name, like needs_sol (the agent needs a little more SOL for fees). Each name has a plain sentence and a yes-or-no answer to "is it safe to just try again?".

Most errors happen before anything is sent, so no money moved. The one to take seriously is outcome_unknown: then check your balance before you do anything else.

How to read an error

The local API, the command line with --json, and the SDK all describe an error the same way:

an error response
{ "ok": false, "network": "mainnet-beta",
  "error": { "code": "needs_sol",
             "message": "the agent wallet holds 0.0050 SOL; a deposit needs about 0.02 SOL for buffer rent (refunded) and fees.",
             "retryable": false,
             "details": { "haveSol": "0.0050", "needSol": "0.02" } },
  "requestId": "req_..." }
ok
false for an error, true for success.
error.code
A short, fixed name. Programs should check this, never the message.
error.message
A sentence for people: what happened and, usually, what to do.
error.retryable
true: nothing was sent and the same request may be tried again later, with the same idempotency key. false: something must change first. Then send it with a new idempotency key: the old key keeps returning this same error.
error.details
Extra facts when there are some, like needSol, the relayer's reason, or the idempotency key.
requestId
The ID of this call in the local server's log. The command line does not print one.

The API also sets the HTTP status (401, 409, 503…) and, for waits, a Retry-After header in seconds.

macOS / Linux (bash)
# a wrong token on purpose: -i shows the HTTP status too
curl -s -i -H "Authorization: Bearer wrong" http://127.0.0.1:8081/v1/balance
Windows PowerShell
# Invoke-RestMethod throws on an error; the body is in ErrorDetails
try {
  Invoke-RestMethod -Headers @{ Authorization = "Bearer wrong" } http://127.0.0.1:8081/v1/balance
} catch {
  $e = ($_.ErrorDetails.Message | ConvertFrom-Json).error
  "$($e.code) (retryable: $($e.retryable)): $($e.message)"
}
Python (requests)
import pathlib, requests

API = "http://127.0.0.1:8081/v1"
token = (pathlib.Path.home() / ".h081" / "api-token").read_text().strip()

r = requests.get(API + "/balance", headers={"Authorization": "Bearer " + token})
data = r.json()
if data["ok"]:
    print(data["result"]["shielded"])
else:
    err = data["error"]
    print(r.status_code, err["code"], err["retryable"], err["message"])
    print(err.get("details"))
TypeScript
import { H081, classify } from '@h081lll/agent';

const bank = await H081.open();
console.log((await bank.deposit('5')).result);   // the plan: nothing is sent
try {
  await bank.deposit('5', { confirm: true, idempotencyKey: 'dep-1' });
} catch (e) {
  const err = classify(e);               // { code, status, retryable, message, details }
  console.log(err.code, err.status, err.retryable, err.message, err.details);
}
response (curl)
HTTP/1.1 401 Unauthorized
content-type: application/json; charset=utf-8
...
{"ok":false,"network":"mainnet-beta","error":{"code":"unauthorized","message":"missing or wrong token: send Authorization: Bearer <the token in ~/.h081/api-token>","retryable":false},"requestId":"req_..."}
Example conversation · wording variesAn error, through Claude
  1. You

    Deposit 5 USDG into my H081 account.

  2. Claude

    uses h081_bank_deposit

    I could not deposit: your agent wallet holds 0.0050 SOL, and a deposit needs about 0.02 SOL for network fees and a small refundable deposit. Nothing was sent.

    Send a little SOL to your agent address and ask me again.

With Claude, a refused action comes back as a tool result with the same code and message, so Claude can explain it and change course.

Every error code

Grouped by what kind of problem it is. "Retry?" says whether the same request may simply be sent again later. On phones, the tables scroll sideways.

Your request

Something about the call itself: the token, the address, the format.

CodeHTTPRetry?What it meansWhat to do
unauthorized401NoThe token is missing or wrong.Send Authorization: Bearer <token>. The token is in ~/.h081/api-token; the token command prints it. After token --rotate, update your scripts.
forbidden_origin403NoThe call came from a web page, or used an address other than 127.0.0.1 or localhost.Call http://127.0.0.1:8081 or http://localhost:8081. For one web page, start serve with --allow-origin <its exact URL>.
not_found404NoNo such route, or the wrong method (GET instead of POST).Check the path and method. GET /v1/openapi.json lists every route.
bad_request400NoA field is missing or wrong. The message names it.Fix that field. Amounts are strings like "2.5". On the command line, a usage mistake (unknown command, missing option) also shows as bad_request, with exit code 2.
unsupported_media_type415NoA POST body was not sent as JSON.Add the header Content-Type: application/json.
invalid_code400NoAn h081-… code failed its checksum, kind or network check: a typo, a cut-off copy, or the wrong kind of code.Copy the whole code again. Check it with the check command or POST /v1/codes/check.

Retries and idempotency keys

The safety net that stops a payment from going out twice.

CodeHTTPRetry?What it meansWhat to do
idempotency_key_required400NoA send with "confirm": true has no Idempotency-Key header, or the key has characters that are not allowed.Add Idempotency-Key: 1 to 128 of A-Z a-z 0-9 _ . : -
idempotency_key_reused422NoThis key was already used for a different request.Use a new key for every new payment.
in_progress409YesA request with this key is still running.Wait 5 seconds (Retry-After) and send the same request with the same key.
outcome_unknown409NoIt may or may not have been sent: the server stopped, or the connection broke mid-send.Do not resend. Check the balance (balance --fresh, GET /v1/balance?fresh=1) first. Only if nothing went out, send again with a new key.

Money, fees and limits

Nothing was sent. Something needs topping up, or the amount is over a limit.

CodeHTTPRetry?What it meansWhat to do
insufficient_shielded409NoYour private balance is too low.Deposit more, or ask for less. Check with balance.
insufficient_wallet_usdg409NoThe agent wallet holds too little public USDG, for example for a deposit.Send USDG to the agent address (the address command prints it).
needs_sol409NoThe agent wallet needs more SOL for network fees. details.needSol says how much.Send SOL to the agent address. See "Not enough SOL" below.
cap_per_deposit409NoOver the per-deposit cap (1,000 USDG on mainnet).Split it into smaller deposits.
cap_total409NoThe bank is at its total cap (10,000 USDG on mainnet).Deposit less, or later.
deposits_paused409NoNew deposits are paused. Withdrawals still work.Try the deposit later.
over_operator_ceiling409NoOver a ceiling set on this computer (H081_MAX_DEPOSIT, H081_MAX_PAY, H081_MAX_GRANT or H081_MAX_LEND).Ask for less, or raise that setting (in base units: 1 USDG = 1000000).
needs_stake409NoAbove what the relayer sends with nothing staked (100 USDG per withdrawal).Split the amount, stake H081, or use --direct (your agent wallet becomes public as the sender).
allowance_blocked409NoNo budget can pay this amount now. The message names the limit that blocks it: per payment, per day (window), expiry, what is left, or the part not unlocked yet.Pay less, wait for the window to reset or more of the budget to unlock, or ask the owner for a new budget. balance lists your budgets and their limits.

Network, relayer and timing

Usually brief. Most are safe to retry with the same key.

CodeHTTPRetry?What it meansWhat to do
relayer_unavailable503YesThe relayer cannot be reached, or refused. Nothing was sent. details says why.Retry later with the same key. Only if you accept a public sender, use --direct.
relayer_busy503YesThe relayer is busy. Nothing was sent. It was already retried once for you.Wait a few seconds, then retry with the same key.
rate_limited429YesToo many requests: the relayer said so, or this computer passed 30 sends a minute (H081_API_MAX_SENDS_PER_MIN).Wait (the Retry-After header says how long when it is set; otherwise a few seconds), then retry with the same key.
busy503YesThe local queue is full (8 sends are already waiting).Wait 5 seconds (Retry-After) and retry.
stale_root409YesThe bank moved on while the proof was being made (a newer Merkle root or timestamp).Retry with the same key. See "Stale root" below.
in_flight409YesSome of your money in this operation is already being spent by another operation.Let the other one finish, then retry.
rpc_unreachable502YesThe Solana RPC cannot be reached.Check the URL, or set your own with init --rpc or H081_RPC_URL.

On-chain refusals

A Solana program said no. The message explains which rule.

CodeHTTPRetry?What it meansWhat to do
already_spent409NoMoney in this operation was already spent, often by another operation from the same account at the same time.Check balance --fresh, then make a new request.
simulation_failed422NoThe transaction failed a dry run. Nothing was sent.Read the message; it says what the program objected to.
program_error422NoAn on-chain program refused it. When known, details names the program and its error.Read the message: it names the rule and, for the common ones, what to change.

Setup

Something on this computer is missing.

CodeHTTPRetry?What it meansWhat to do
circuits_missing503NoThe proof files are not downloaded yet.Run fetch-circuits. See "Proof files missing" below.
circuit_hash_mismatch503NoA downloaded proof file does not match its pinned fingerprint: damaged, or not from H081.Delete ~/.h081/circuits and run fetch-circuits again.
no_key500NoThere is no agent key.Run init, or restore from your backup code.
not_available501NoNot available in this version.This version cannot do it. Check for a newer version of @h081lll/agent.
internal500NoSomething unexpected.Check the balance before trying a send again. Keep the requestId; the server log names it.

Common situations

Not enough SOL (needs_sol)

SOL is Solana's own coin. Every action pays a small network fee in SOL, and some stage a proof on chain first, which needs a refundable deposit. The error tells you what the wallet has (details.haveSol) and roughly what it needs (details.needSol).

To useKeep in the agent wallet
Deposit and withdraw0.05 SOL
Budgets (allowances) or lending0.15 SOL
  1. Find the agent's address

    any terminal
    npx -y @h081lll/agent address
  2. Send SOL to it

    From any Solana wallet or exchange. Send a little more than needSol.

  3. Check it arrived, then see the plan again

    any terminal
    npx -y @h081lll/agent balance --fresh
    the plan (nothing is sent); add --yes when it looks right
    npx -y @h081lll/agent deposit 5

    From the API or the SDK, send it with a new idempotency key. The key you used before keeps returning needs_sol. (The command line makes a new key on every run.)

Relayer busy or unavailable

The relayer sends withdrawals for you. When it is busy (relayer_busy) or answers "too many requests" (rate_limited), H081 already waited and tried once more before giving you the error. Nothing was sent.

response
{ "ok": false, "network": "mainnet-beta",
  "error": { "code": "relayer_busy", "message": "...", "retryable": true,
             "details": { "relayerStatus": 503, "reason": "busy" } },
  "requestId": "req_..." }
  1. Wait a few seconds

    Use Retry-After when the response has it.

  2. Send the same request with the same idempotency key

    The key was released because nothing was sent, so it runs again. If it had gone through, you would get the stored answer instead: it can never pay twice.

    command line: the plan first (nothing is sent)
    npx -y @h081lll/agent withdraw 2 --to <a wallet that never touched the agent>
    then send, passing the key yourself so a rerun is the same request
    npx -y @h081lll/agent withdraw 2 --to <a wallet that never touched the agent> --yes --idempotency-key wd-7
  3. Still down after a while?

    If the relayer stays unreachable (relayer_unavailable), wait and try later. --direct skips the relayer, but then your agent wallet is public as the sender.

A retry loop that is safe to copy: it retries only when retryable is true, keeps the same key, and stops at outcome_unknown.

Python (requests)
import pathlib, time, 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()

def send(path, body, key, tries=3):
    """A confirmed send that retries only when the API says it is safe."""
    for attempt in range(tries):
        r = s.post(API + path, json={**body, "confirm": True}, headers={"Idempotency-Key": key})
        data = r.json()
        if data["ok"]:
            return data["result"]
        err = data["error"]
        if err["code"] == "outcome_unknown":
            raise RuntimeError("It may have been sent. Check GET /v1/balance?fresh=1 before anything else.")
        if not err["retryable"] or attempt == tries - 1:
            raise RuntimeError(err["code"] + ": " + err["message"])
        time.sleep(int(r.headers.get("Retry-After", "5")))   # same key: it can never send twice

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

key = str(uuid.uuid4())                                       # one key per payment
print(send("/withdraw", body, key))
TypeScript
import { H081, classify } from '@h081lll/agent';

const bank = await H081.open();
const to = '<fresh wallet>';
console.log((await bank.withdraw('2', { to })).result);       // the plan: nothing is sent
const key = crypto.randomUUID();                              // one key per payment

for (let attempt = 1; ; attempt++) {
  try {
    console.log(await bank.withdraw('2', { to, confirm: true, idempotencyKey: key }));
    break;
  } catch (e) {
    const err = classify(e);
    if (err.code === 'outcome_unknown') { console.log('Maybe sent. Check first:', await bank.balance({ fresh: true })); break; }
    if (!err.retryable || attempt === 3) throw e;
    await new Promise((r) => setTimeout(r, 5000));           // same key: it can never send twice
  }
}

Outcome unknown (outcome_unknown)

It happens when the local server stopped in the middle of a send, or the connection to the relayer or Solana broke while sending. It is never marked retryable, and the same key keeps giving this answer, so a script cannot pay twice by accident.

  1. Check the balance and recent history

    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.

  2. Decide

    Balance dropped, or the money arrived: it went through. Do nothing. Balance unchanged and nothing arrived: send it again with a new idempotency key.

Stale root (stale_root)

A proof is made against a snapshot of the bank (its "Merkle root" and a timestamp). If other people used the bank while your proof was being made, the snapshot is out of date and the program refuses it. Nothing was sent.

response
{ "ok": false, "network": "mainnet-beta",
  "error": { "code": "stale_root", "message": "...", "retryable": true, "details": { ... } },
  "requestId": "req_..." }

Fix: send the same request again; the same idempotency key is fine. A new proof is made against the current snapshot. in_flight is similar: another operation from your account is spending the same money right now, so let it finish first.

Proof files missing or damaged

Proofs need proving files (about 98 MB in total), downloaded once into ~/.h081/circuits and checked against pinned fingerprints. serve downloads them in the background; the first proof also fetches what it needs.

circuits_missing: download them.

any terminal
npx -y @h081lll/agent fetch-circuits

circuit_hash_mismatch: a file is damaged or not from H081, and it is refused. Delete the folder and download again. If it happens again, tell us at @H081CAP.

macOS / Linux (bash)
rm -rf ~/.h081/circuits
npx -y @h081lll/agent fetch-circuits
Windows PowerShell
Remove-Item -Recurse -Force $HOME\.h081\circuits
npx -y @h081lll/agent fetch-circuits

Cannot reach Solana (rpc_unreachable) or "429 Too Many Requests"

The RPC is the Solana server your computer talks to. The public one limits how often you can ask. Use your own RPC URL (a free tier is enough):

any terminal (keeps your existing key)
npx -y @h081lll/agent init --rpc <your Solana RPC URL>

Or set H081_RPC_URL for one program. Only the RPC's host name is ever shown in messages and logs.

Command line exit codes

Every command ends with a number that scripts can check:

Exit codeMeaning
0It worked (or it printed a plan).
1An error. With --json, the error envelope is on stdout.
2Bad usage: an unknown command or a missing or wrong option. With --json, stdout has a bad_request envelope.

With --json, stdout (the command's normal output) carries only the JSON envelope; messages and logs go to stderr (its separate error output), so you can pipe stdout into another program.

a send that fails (run it without --yes first to see the plan)
npx -y @h081lll/agent deposit 5 --yes --json
stdout (exit code 1)
{
  "ok": false,
  "network": "mainnet-beta",
  "error": {
    "code": "needs_sol",
    "message": "the agent wallet holds 0.0050 SOL; a deposit needs about 0.02 SOL for buffer rent (refunded) and fees.",
    "retryable": false,
    "details": { "haveSol": "0.0050", "needSol": "0.02" }
  }
}
macOS / Linux (bash)
npx -y @h081lll/agent balance --json > balance.json
echo "exit code: $?"
Windows PowerShell
npx -y @h081lll/agent balance --json > balance.json
"exit code: $LASTEXITCODE"

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.