Local API

A small bank API that runs on your own computer, on 127.0.0.1:8081. Any program can call it: curl, PowerShell, Python, TypeScript. Every route, field, response and error, with examples.

In plain words

The local API is a small server on your own computer that any program can talk to. You start it with one command. Then a script, a bot or an app you write can ask it things like "what is my balance?" or "send 2 USDG to this wallet", in any programming language.

It only answers programs on the same computer, and only when they show a secret password (the token). Your key never leaves your computer.

Nothing moves by accident. When you ask it to send money, it first answers with a plan: the fee, what arrives, what the public can see. It sends only when you ask again and add "confirm": true.

No agent key yet? Start with Getting started. Rather not write code? Use it with Claude or the command line. Writing Node.js? The TypeScript SDK does the same without a server.

All 21 routes at a glance

Every route lives under http://127.0.0.1:8081/v1. GET routes only read. POST routes that move money return a plan first. Click a route for its fields and examples.

RouteWhat it does
GET /v1/healthIs the server running? (no token)
GET /v1/openapi.jsonThe full machine-readable description (no token)
GET /v1/statusWhere things stand, without asking the chain
GET /v1/addressThe agent's wallet and its h081-address: code
GET /v1/balancePrivate balance, wallet balance, budgets
GET /v1/historyWhat this computer sent
GET /v1/fees/withdrawThe fee for a withdrawal, before you make it
POST /v1/depositWallet → private balance
POST /v1/withdrawPrivate balance → any wallet
POST /v1/payPay from a budget this agent holds
POST /v1/transferPrivate balance → another H081 account
POST /v1/allowancesGive a budget
POST /v1/allowances/{id}/reclaimTake back what is left of a budget
GET /v1/loans/quotePrice a loan from a lender's terms
POST /v1/loans/borrowPledge a budget, get a proof code for the lender
POST /v1/loans/lendLender actions: open, fund, collect, claim, settle
GET /v1/loans/lentYour loan slots as a lender
GET /v1/stakeYour H081 stake
POST /v1/stakeStake or unstake H081
POST /v1/codes/checkCheck any h081-* code
GET /v1/eventsLive updates (server-sent events)

The machine-readable version of everything on this page: h081-agent-openapi.json (OpenAPI 3.1).

Start the server

  1. Start it, and leave it running

    Run this in a terminal and keep that window open. It listens on 127.0.0.1:8081, a private address only your own computer can reach. In the background it also downloads the proof files (about 98 MB, once, checked against fixed fingerprints) so the first payment does not wait.

    terminal 1 · any system
    npx -y @h081lll/agent serve
    what it prints (in the terminal, not to programs)
    H081 gateway 0.1.0 on http://127.0.0.1:8081/v1 (mainnet-beta, agent 7xKX…, RPC <your RPC host>)
    token: /home/you/.h081/api-token   docs: http://127.0.0.1:8081/v1/openapi.json   …

    Press Ctrl+C to stop it. It uses the key in ~/.h081/agent.json and the settings in ~/.h081/config.json, both written by npx -y @h081lll/agent init.

  2. Check it answers

    Open a second terminal. This route needs no token, so it is a quick way to see the server is up.

    terminal 2 · macOS / Linux
    curl -s http://127.0.0.1:8081/v1/health
    terminal 2 · Windows PowerShell
    Invoke-RestMethod http://127.0.0.1:8081/v1/health | ConvertTo-Json -Depth 5
    response
    { "ok": true, "network": "mainnet-beta", "status": "done",
      "result": { "version": "0.1.0", "network": "mainnet-beta" },
      "requestId": "req_3f9c2a71d0b84e15" }
  3. Make your first real call

    Every other route needs the token. Set up once shows how to read it in each language.

Options

FlagDefaultWhat it does
--port <n>8081The port (1 to 65535). The environment variable H081_API_PORT sets the same thing.
--poll <seconds>20How often it re-reads the chain for live events, only while a listener is connected. At least 1.
--allow-origin <url>noneLets exactly one web page call the API from a browser. See Local security.
examples
npx -y @h081lll/agent serve --port 8082
npx -y @h081lll/agent serve --poll 5
npx -y @h081lll/agent serve --allow-origin http://localhost:5173

There is no option to listen on any address other than 127.0.0.1. Asking for one (--host or --bind) is refused.

Set up once: the token

In plain words: the token is a long random password, saved in a file on your computer. Programs send it with every call so the server knows the call comes from you.

  • It lives in ~/.h081/api-token (only your user can read it). init and serve create it if it is missing.
  • Send it as the header Authorization: Bearer <token>.
  • Only GET /v1/health and GET /v1/openapi.json work without it.

Pick your language. The examples further down build on these lines: $TOKEN in bash, $h in PowerShell, and the call() helper in Python and TypeScript.

macOS / Linux (bash)
# read the token once per terminal
TOKEN=$(cat ~/.h081/api-token)

curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8081/v1/balance
Windows PowerShell
# read the token once per window
$token = (Get-Content "$HOME\.h081\api-token" -Raw).Trim()
$h = @{ Authorization = "Bearer $token" }

Invoke-RestMethod -Headers $h http://127.0.0.1:8081/v1/balance
Python (requests) · the call() helper used below
# pip install 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 call(method, path, body=None, key=None, **kw):
    """One API call. Returns the whole envelope; raises on an error envelope."""
    headers = {"Idempotency-Key": key} if key else {}
    r = s.request(method, API + path, json=body, headers=headers, **kw).json()
    if not r["ok"]:
        raise RuntimeError(r["error"])          # code, message, retryable, details
    return r

print(call("GET", "/balance")["result"]["shielded"])
TypeScript · the call() helper used below
// h081.mts · Node.js 20+ (an ES module, so top-level await works)
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';

const API = 'http://127.0.0.1:8081/v1';
const TOKEN = readFileSync(join(homedir(), '.h081', 'api-token'), 'utf8').trim();

async function call(method: 'GET' | 'POST', path: string, body?: object, key?: string) {
  const headers: Record<string, string> = { Authorization: `Bearer ${TOKEN}` };
  if (body) headers['Content-Type'] = 'application/json';
  if (key) headers['Idempotency-Key'] = key;
  const res = await fetch(API + path, { method, headers, body: body ? JSON.stringify(body) : undefined });
  const env = await res.json();
  if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message}`);
  return env;
}

console.log((await call('GET', '/balance')).result.shielded);

See or replace the token

print the token
npx -y @h081lll/agent token
replace it with a new one
npx -y @h081lll/agent token --rotate

After --rotate, restart serve and update your scripts: the old token stops working. A missing or wrong token gets this answer (HTTP 401):

response · 401
{ "ok": false, "network": "mainnet-beta",
  "error": { "code": "unauthorized", "retryable": false,
             "message": "missing or wrong token: send Authorization: Bearer <the token in ~/.h081/api-token>" },
  "requestId": "req_…" }

What every answer looks like

In plain words: every answer is a small JSON document with the same shape. ok tells you whether it worked. If it did, the data is in result. If not, error says what went wrong and whether trying again can help.

success (← notes are not part of the JSON)
{
  "ok": true,
  "network": "mainnet-beta",
  "status": "done",            ← done (a read) · plan (nothing sent) · sent (it went out)
  "result": { … },             ← what the route returns
  "replayed": true,            ← only when an Idempotency-Key replay returned a stored result
  "requestId": "req_…"         ← also in the X-Request-Id header and the server's log
}
error (← notes are not part of the JSON)
{
  "ok": false,
  "network": "mainnet-beta",
  "error": {
    "code": "needs_sol",       ← a fixed code: switch on this, not on the message
    "message": "…",            ← for people
    "retryable": false,        ← true: the same request may work if you wait and try again
    "details": { "needSol": "…" }
  },
  "requestId": "req_…"
}
FieldMeaning
oktrue or false.
networkmainnet-beta (the real one), or localnet on a test validator.
statusdone: a read finished. plan: a write was described and nothing was sent. sent: it went out.
resultWhat the route returns. Each route below shows it.
replayedOnly present, as true, when a retry with the same Idempotency-Key got the stored answer back instead of sending again.
errorcode (fixed, for programs), message (for people), retryable, and sometimes details. All codes: Errors and fixes.
requestIdAn id like req_3f9c…, also sent as the X-Request-Id header and written in the server's log line.

Amounts

Amounts are USDG, written as decimal strings: "usdg": "5.25". Where a route takes usdg, you can give amount in base units instead ("5250000" = 5.25 USDG, 6 decimals), but never both. In results, fields ending in Usdg (like receivedUsdg) are decimal; the same name without it (received) is in base units.

A POST body must be a JSON object, at most 64 KB, sent with Content-Type: application/json. A field a route does not take is refused with bad_request, and the message lists the fields it does take.

Plan first, then confirm

In plain words: asking the server to send money is a two-step conversation. First it tells you exactly what would happen. Then, if you agree, you ask again and say "yes, do it".

  1. Ask without confirm: get the plan

    Send the request without confirm (or with "confirm": false). You get "status": "plan". Nothing is proven, signed or sent, so this is always safe to try.

  2. Read the plan

    Plan fieldMeaning
    op, usdg, toWhat, how much, to whom.
    feeUSDG taken out of the amount.
    payoutUSDG the recipient gets.
    solNeededSOL the agent wallet needs for this (network fees).
    circuit, downloadWhich proof it makes, and what the first one downloads (once).
    publicOnChainWhat anyone can see on the blockchain afterwards.
    sentByrelayer (H081's relayer sends it, so your agent wallet is not the sender) or agent key.
    warningsThings to know first, in plain sentences.
    detailsExtra detail for budgets, loans and staking.
    nextAlways: "Nothing was sent. Send it again with confirm: true (CLI: --yes) to do it."
  3. Send it: confirm + Idempotency-Key

    Send the same request with "confirm": true and a header Idempotency-Key. Without that header the server refuses (400 idempotency_key_required), so that a retry can never pay twice. The answer is "status": "sent" with the transaction signature.

Idempotency keys: never pay twice

In plain words: an idempotency key is a label you put on one payment, like an order number. If your program crashes or the network drops and you are not sure the payment went out, you send it again with the same label. The server recognises the label and gives you the first answer back instead of paying a second time.

  • Required on every write with "confirm": true. Header: Idempotency-Key: <key>.
  • 1 to 128 characters from A-Z a-z 0-9 _ . : -. A random UUID works well.
  • One new key for each new payment. Reuse a key only to retry that same payment with the same body.
  • Keys are remembered for 7 days in ~/.h081/ops.jsonl (the command line and the server share this file).
What happensWhat the server answers
Same key, same body, already finishedThe stored answer again, with "replayed": true. Nothing is sent again.
Same key, still running409 in_progress, header Retry-After: 5. Wait and ask again with the same key.
Same key, different body or route422 idempotency_key_reused. Use a new key.
The server stopped while it ran, or the connection to the relayer or the Solana RPC failed in the middle of sending409 outcome_unknown: it may have been sent. The same key keeps answering outcome_unknown and is never sent again. Check GET /v1/balance?fresh=1 before you try again with a new key.
A retryable error where nothing was sent (relayer busy, rate limited, a stale proof root)The key is released: the same request may be sent again with the same key.
Python · a safe retry loop (uses the setup above)
# You already asked without confirm and read the plan. Now send it, safely.
key = str(uuid.uuid4())                    # ONE key for this ONE payment
body = {"usdg": "2", "to": "<fresh wallet>", "confirm": True}

for attempt in range(4):
    resp = s.post(API + "/withdraw", json=body, headers={"Idempotency-Key": key})
    r = resp.json()
    if r["ok"] or not r["error"]["retryable"]:
        break
    time.sleep(int(resp.headers.get("Retry-After", "5")))   # same key: it can never go out twice

if not r["ok"] and r["error"]["code"] == "outcome_unknown":
    print("It may have been sent. Check the balance before trying again with a NEW key:")
    print(call("GET", "/balance", params={"fresh": "1"})["result"]["shielded"])
else:
    print(r)

Queue and limits

In plain words: the server does one payment at a time, in order, and refuses to be flooded.

LimitValueWhen you hit it
Proofs at once1 (one queue: one proof and one send at a time)Writes wait their turn.
Writes waiting8503 busy with Retry-After: 5.
Sends per minute30 (H081_API_MAX_SENDS_PER_MIN)429 rate_limited with Retry-After in seconds.
Balance cache15 secondsGET /v1/balance answers from memory; add ?fresh=1 to read the chain now.
Relayer busy or rate limitedretried onceOnly then is the error returned.
response · 503 busy
HTTP/1.1 503 Service Unavailable
Retry-After: 5

{ "ok": false, "network": "mainnet-beta",
  "error": { "code": "busy", "message": "8 sends are already waiting; try again shortly",
             "retryable": true, "details": { "retryAfter": 5 } },
  "requestId": "req_…" }

Plans and reads do not count toward the send limit; only confirmed writes do. You can also set ceilings, enforced before anything is proven: H081_MAX_DEPOSIT, H081_MAX_PAY, H081_MAX_GRANT and H081_MAX_LEND (base units). Going over gives 409 over_operator_ceiling.

Local security

In plain words: the server only listens to your own computer, and it refuses web pages unless you name one.

  • 127.0.0.1 only. It never listens on your network. There is no option to change that.
  • Host check. The Host header must be 127.0.0.1:<port> or localhost:<port>. This blocks a trick called DNS rebinding, where a website pretends to be your own computer. Otherwise: 403 forbidden_origin.
  • Origin check. Browsers add an Origin header to calls from web pages. Any request with an Origin is refused (403 forbidden_origin) unless it equals --allow-origin exactly. No CORS headers are sent otherwise. curl, scripts and the SDK send no Origin, so they just work.
  • JSON only. A POST must be Content-Type: application/json (otherwise 415 unsupported_media_type), so a plain web form cannot post to it.
  • Token. Compared in constant time, so it cannot be guessed by timing.
  • Logs. Keys, tokens, notes and full RPC URLs are never logged; only the RPC host is shown.

Calling it from your own web page

Only for a page you wrote and trust, running on your own computer. Start the server with that page's exact origin:

terminal 1
npx -y @h081lll/agent serve --allow-origin http://localhost:5173
JavaScript in that page
// in the page served from http://localhost:5173 (and only that exact origin)
const token = '<the token from ~/.h081/api-token>';
const r = await fetch('http://127.0.0.1:8081/v1/balance', { headers: { Authorization: 'Bearer ' + token } });
console.log((await r.json()).result.shielded);

The server then answers that origin's preflight requests, allowing GET, POST and the headers Authorization, Content-Type, Idempotency-Key and Last-Event-ID. The page needs the token, so anyone who can change that page can spend.

Reading: status, balance, history, fees

These routes only look. They never send anything.

GET/v1/health

Is the server running? Returns its version and network.

No token needed#

Use it to check the server is up before anything else. No token, no chain access.

curl
curl -s http://127.0.0.1:8081/v1/health
response
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "version": "0.1.0", "network": "mainnet-beta" },
  "requestId": "req_3f9c2a71d0b84e15" }

GET/v1/openapi.json

The OpenAPI 3.1 description of every route, field and error code.

No token needed#

The raw document, not wrapped in the ok/result envelope. Load it into any OpenAPI tool to browse the API or generate a client. The same file is on this site: /h081-agent-openapi.json.

curl
curl -s http://127.0.0.1:8081/v1/openapi.json

GET/v1/status

Where things stand: network, agent, key source, RPC host, proof files, queue. No chain call.

Needs the token#

Quick and offline: it does not ask the blockchain anything. circuits.missing lists proof files not downloaded yet; queue.depth counts writes waiting or running; fundingNeeded is the SOL to keep in the agent wallet for each kind of use.

curl
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8081/v1/status
response (example)
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": {
    "network": "mainnet-beta",
    "agent": "7xKX…",
    "keySource": "file /home/you/.h081/agent.json",
    "rpcHost": "<your RPC host>",
    "relayer": "api.h081.xyz",
    "circuits": { "present": ["transact", "spend"], "missing": ["grant", "stream"] },
    "syncedAt": null,
    "syncing": false,
    "queue": { "depth": 0 },
    "fundingNeeded": { "deposit": "0.05", "withdraw": "0.05", "allow": "0.15", "lend": "0.15" }
  },
  "requestId": "req_…" }

GET/v1/address

The agent wallet and its h081-address: code. Offline.

Needs the token#

wallet is the public Solana address to send SOL and USDG to. addressCode is what you give someone who should pay this agent privately, or give it a budget.

curl
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8081/v1/address
response
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "wallet": "7xKX…", "addressCode": "h081-address:…" },
  "requestId": "req_…" }

GET/v1/balance

Private (shielded) balance, wallet balance, deposit caps and budgets.

Needs the token#

QueryWhat it does
fresh=1Read the chain now. Without it, an answer under 15 seconds old comes from memory.

shielded is the private balance. wallet is what the agent's public wallet holds. Each entry in allowances is a budget: its id (which changes after every payment from it), remaining, who holds it (holder), its caps and end date, whether you can take it back (reclaimable) and whether it backs a loan (pledged).

macOS / Linux (bash)
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8081/v1/balance

# skip the 15-second cache and read the chain now
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8081/v1/balance?fresh=1"
Windows PowerShell
$r = Invoke-RestMethod -Headers $h "http://127.0.0.1:8081/v1/balance?fresh=1"
"Private balance: $($r.result.shielded) USDG"
"Agent wallet:    $($r.result.wallet.usdg) USDG, $($r.result.wallet.sol) SOL"
$r.result.allowances | Format-Table id, remaining, holder, reclaimable
Python (requests)
r = call("GET", "/balance", params={"fresh": "1"})["result"]
print("private:", r["shielded"], "USDG")
print("wallet: ", r["wallet"]["usdg"], "USDG,", r["wallet"]["sol"], "SOL")
for a in r["allowances"]:
    print("budget", a["id"], a["remaining"], "left,", a["holder"])
TypeScript
const r = (await call('GET', '/balance?fresh=1')).result;
console.log(`private: ${r.shielded} USDG, wallet: ${r.wallet.usdg} USDG and ${r.wallet.sol} SOL`);
for (const a of r.allowances) console.log('budget', a.id, a.remaining, 'left,', a.holder);
response (example)
{
  "ok": true,
  "network": "mainnet-beta",
  "status": "done",
  "result": {
    "shielded": "3",
    "wallet": { "usdg": "0", "sol": "0.0412" },
    "caps": { "perDeposit": "1000", "total": "10000", "inVault": "…", "paused": false },
    "allowances": [
      { "id": "18340563…", "remaining": "20", "holder": "sub-account 0", "agentIndex": 0,
        "dailyCap": "5", "maxPerTx": "2", "expires": "2026-12-11T09:30:00.000Z",
        "reclaimable": true, "pledged": false }
    ],
    "addressCode": "h081-address:…",
    "syncedAt": "2026-11-11T09:30:00.000Z"
  },
  "requestId": "req_…"
}

GET/v1/history

What this server or the command line sent from this computer, newest first.

Needs the token#

QueryWhat it does
limitItems per page, 1 to 200. Default 50.
afterThe next value from the previous page. next is null on the last page.

It comes from ~/.h081/ops.jsonl, so it shows only what this computer sent, in the last 7 days. Each item's id is the idempotency key it was sent with, and state is pending, done or failed (with an error code).

curl
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8081/v1/history?limit=20"

# the next page: pass the "next" value you got back
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8081/v1/history?limit=20&after=wd-1"
response (example)
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": {
    "items": [
      { "id": "wd-1", "time": "2026-11-11T09:31:00.000Z", "kind": "withdraw", "usdg": "2",
        "signature": "…", "state": "done", "to": "<fresh wallet>" },
      { "id": "dep-1", "time": "2026-11-11T09:20:00.000Z", "kind": "deposit", "usdg": "5",
        "signature": "…", "state": "done" }
    ],
    "next": null
  },
  "requestId": "req_…" }

GET/v1/fees/withdraw

The relayer's fee for a withdrawal, and what arrives. No proof is made.

Needs the token#

QueryWhat it does
usdg or amountThe amount to withdraw, in decimal USDG or base units.

payout is what arrives. freeTierMax is the most the relayer sends with nothing staked (100 USDG); needsStake says whether this amount is over it. createAccountFee, when present, is added if the recipient wallet has no USDG account yet.

curl
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8081/v1/fees/withdraw?usdg=2"
response (example)
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "fee": "0.05", "feeBps": 20, "minFee": "0.05", "payout": "1.95",
              "freeTierMax": "100", "needsStake": false, "relayer": "https://api.h081.xyz",
              "createAccountFee": "1" },
  "requestId": "req_…" }

Moving money: deposit, withdraw, pay, transfer

All four return a plan first and send only with "confirm": true and an Idempotency-Key.

POST/v1/deposit

Move USDG from the agent wallet into the private (shielded) balance.

Needs the tokenPlan first · sends only with "confirm": true#

In plain words: puts money from the agent's public wallet into the bank. The deposit itself is public (the chain shows the agent wallet and the amount); what happens inside afterwards is private.

FieldTypeWhat it is
usdg or amountstringHow much: decimal USDG, or base units. Exactly one.
confirmbooleantrue sends it. Missing or false: the plan only.
macOS / Linux (bash)
# 1 · the plan: nothing is sent
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"usdg":"5"}' http://127.0.0.1:8081/v1/deposit

# 2 · send it: "confirm": true and an Idempotency-Key (keep KEY if you need to retry)
KEY=dep-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d '{"usdg":"5","confirm":true}' http://127.0.0.1:8081/v1/deposit
Windows PowerShell
# 1 · the plan: nothing is sent
$body = @{ usdg = "5" } | ConvertTo-Json
Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body $body http://127.0.0.1:8081/v1/deposit

# 2 · send it (keep $key if you need to retry)
$key = [guid]::NewGuid().ToString()
$body = @{ usdg = "5"; confirm = $true } | ConvertTo-Json
Invoke-RestMethod -Method Post -Headers ($h + @{ "Idempotency-Key" = $key }) -ContentType application/json -Body $body http://127.0.0.1:8081/v1/deposit
Python (requests)
plan = call("POST", "/deposit", {"usdg": "5"})
print(plan["status"], plan["result"]["warnings"])        # "plan": nothing is sent

sent = call("POST", "/deposit", {"usdg": "5", "confirm": True}, key=str(uuid.uuid4()))
print(sent["result"]["signature"], sent["result"]["shieldedUsdg"])
TypeScript
const plan = await call('POST', '/deposit', { usdg: '5' });   // nothing is sent
console.log(plan.status, plan.result.warnings);

const sent = await call('POST', '/deposit', { usdg: '5', confirm: true }, randomUUID());
console.log(sent.result.signature, sent.result.shieldedUsdg);
response without confirm: the plan
{ "ok": true, "network": "mainnet-beta", "status": "plan",
  "result": { "op": "deposit", "usdg": "5", "fee": "0", "solNeeded": "0.02", "circuit": "transact",
              "download": "15 MB", "publicOnChain": ["agent wallet", "amount"], "sentBy": "agent key",
              "warnings": ["A deposit is public: the chain shows the agent wallet and the amount. What happens inside the bank afterwards is private."],
              "next": "Nothing was sent. Send it again with confirm: true (CLI: --yes) to do it." },
  "requestId": "req_…" }
response with confirm (example, shortened)
{ "ok": true, "network": "mainnet-beta", "status": "sent",
  "result": { "signature": "…", "amount": "5000000", "amountUsdg": "5",
              "shielded": "5000000", "shieldedUsdg": "5", … },
  "requestId": "req_…" }

Deposits are capped at 1,000 USDG each and 10,000 USDG in the whole bank for now (cap_per_deposit, cap_total). The first deposit downloads its 15 MB proof file if serve has not already.

POST/v1/withdraw

Send USDG from the private balance to any wallet, through the H081 relayer.

Needs the tokenPlan first · sends only with "confirm": true#

In plain words: takes money out of the bank to a wallet. H081's relayer sends it, so the chain shows the recipient and the amount, but not your agent's wallet.

FieldTypeWhat it is
usdg or amountstringHow much leaves the bank. The fee comes out of it.
to (required)stringA Solana wallet address. Use a fresh one. "self" means the agent wallet; the plan then warns that this links the two.
directbooleantrue: the agent key sends it itself. No relay fee and no relayer limit, but the agent wallet shows as the sender. Default false: through the relayer, and refused (not sent) if the relayer is unavailable.
confirmbooleantrue sends it.
macOS / Linux (bash)
# 1 · the plan: fee, what arrives, what is public (nothing is sent)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"usdg":"2","to":"<fresh wallet>"}' http://127.0.0.1:8081/v1/withdraw

# 2 · send it
KEY=wd-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d '{"usdg":"2","to":"<fresh wallet>","confirm":true}' http://127.0.0.1:8081/v1/withdraw
Windows PowerShell
$to = "<fresh wallet>"
# 1 · the plan (nothing is sent)
$body = @{ usdg = "2"; to = $to } | ConvertTo-Json
(Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body $body http://127.0.0.1:8081/v1/withdraw).result

# 2 · send it
$key = [guid]::NewGuid().ToString()
$body = @{ usdg = "2"; to = $to; confirm = $true } | ConvertTo-Json
Invoke-RestMethod -Method Post -Headers ($h + @{ "Idempotency-Key" = $key }) -ContentType application/json -Body $body http://127.0.0.1:8081/v1/withdraw
Python (requests)
to = "<fresh wallet>"                     # a wallet that never touched the agent
print(call("GET", "/fees/withdraw", params={"usdg": "2"})["result"])

plan = call("POST", "/withdraw", {"usdg": "2", "to": to})
print(plan["result"]["fee"], plan["result"]["payout"], plan["result"]["warnings"])   # nothing is sent

sent = call("POST", "/withdraw", {"usdg": "2", "to": to, "confirm": True}, key=str(uuid.uuid4()))
print(sent["result"]["signature"], sent["result"]["receivedUsdg"])
TypeScript
const to = '<fresh wallet>';                       // a wallet that never touched the agent
const plan = await call('POST', '/withdraw', { usdg: '2', to });   // nothing is sent
console.log(plan.result.fee, plan.result.payout, plan.result.warnings);

const sent = await call('POST', '/withdraw', { usdg: '2', to, confirm: true }, randomUUID());
console.log(sent.result.signature, sent.result.receivedUsdg);
response without confirm: the plan (example)
{ "ok": true, "network": "mainnet-beta", "status": "plan",
  "result": { "op": "withdraw", "usdg": "2", "to": "<fresh wallet>", "fee": "0.05", "payout": "1.95",
              "solNeeded": "0", "circuit": "transact", "download": "0 MB (already downloaded)",
              "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. Send it again with confirm: true (CLI: --yes) to do it." },
  "requestId": "req_…" }
response with confirm (example, shortened)
{ "ok": true, "network": "mainnet-beta", "status": "sent",
  "result": { "signature": "…", "amount": "2000000", "fee": "50000", "received": "1950000",
              "receivedUsdg": "1.95", "to": "<fresh wallet>", "via": "relayer",
              "shielded": "3000000", "shieldedUsdg": "3", … },
  "requestId": "req_…" }
  • The fee is 0.20% or 0.05 USDG, whichever is more. Check it first with GET /v1/fees/withdraw.
  • Up to 100 USDG per relayed withdrawal with nothing staked; above that you get 409 needs_stake. Split it, stake H081, or use direct.
  • A wallet that has never held USDG has no USDG account: the relayer creates it for 1 USDG more. The plan says so.

POST/v1/pay

Pay from a budget (allowance) this agent holds, to a wallet or an h081-address: code.

Needs the tokenPlan first · sends only with "confirm": true#

In plain words: spends from a budget someone gave this agent, inside that budget's limits. Pay an h081-address: code to keep it private, or a wallet for a public payout.

FieldTypeWhat it is
usdg or amountstringHow much.
toAddressCodestringPrivate payment to this h081-address: code. Sent by the agent key: the chain shows the agent wallet made a payment, not to whom or how much.
tostringPublic payout to this Solana wallet (through the relayer). Give exactly one of to or toAddressCode.
allowanceIdstringWhich budget to spend (its id from GET /v1/balance). Default: picked for you.
agentIndexinteger 0 to 15Spend a budget held by this sub-account.
directbooleanPublic payout only: the agent key sends it (no relay fee, sender visible).
allowPledgedbooleanDefault false: a budget that backs a loan is never used. true accepts one; its loan slot and the lender's share then go on chain next to the payment, linking the recipient to the borrower wallet.
confirmbooleantrue sends it.
macOS / Linux (bash)
# 1 · the plan: pay 1 USDG privately to another H081 account (nothing is sent)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"usdg":"1","toAddressCode":"h081-address:…"}' http://127.0.0.1:8081/v1/pay

# 2 · send it
KEY=pay-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d '{"usdg":"1","toAddressCode":"h081-address:…","confirm":true}' \
  http://127.0.0.1:8081/v1/pay
Windows PowerShell
$code = "h081-address:…"
# 1 · the plan (nothing is sent)
$body = @{ usdg = "1"; toAddressCode = $code } | ConvertTo-Json
(Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body $body http://127.0.0.1:8081/v1/pay).result

# 2 · send it
$key = [guid]::NewGuid().ToString()
$body = @{ usdg = "1"; toAddressCode = $code; confirm = $true } | ConvertTo-Json
Invoke-RestMethod -Method Post -Headers ($h + @{ "Idempotency-Key" = $key }) -ContentType application/json -Body $body http://127.0.0.1:8081/v1/pay
Python (requests)
# private: to an h081-address: code
body = {"usdg": "1", "toAddressCode": "h081-address:…"}
print(call("POST", "/pay", body)["result"])                        # the plan: nothing is sent
sent = call("POST", "/pay", {**body, "confirm": True}, key=str(uuid.uuid4()))
print(sent["result"]["signature"], sent["result"].get("remaining"))

# public payout: to a fresh wallet, from one budget only (the plan).
# The budget id is the long number GET /v1/balance shows.
body = {"usdg": "1", "to": "<fresh wallet>", "allowanceId": "<budget id>"}
print(call("POST", "/pay", body)["result"])
TypeScript
const body = { usdg: '1', toAddressCode: 'h081-address:…' };
console.log((await call('POST', '/pay', body)).result);              // the plan: nothing is sent

const sent = await call('POST', '/pay', { ...body, confirm: true }, randomUUID());
console.log(sent.result.signature);
response without confirm: the plan (example)
{ "ok": true, "network": "mainnet-beta", "status": "plan",
  "result": { "op": "pay", "usdg": "1", "to": "shielded", "fee": "0", "payout": "1",
              "solNeeded": "0.02", "circuit": "spend", "download": "19 MB",
              "publicOnChain": ["that the agent wallet sent a bank transaction"], "sentBy": "agent key",
              "warnings": ["Pays from an allowance this agent holds, inside its caps; when none can pay, the error names the cap.",
                           "A payment to an address code is sent by the agent key: the chain shows the agent wallet made a payment, not to whom or how much."],
              "next": "Nothing was sent. Send it again with confirm: true (CLI: --yes) to do it." },
  "requestId": "req_…" }

The sent result has signature, fee, received, allowanceId, remaining and more. When no budget can pay the amount right now, you get 409 allowance_blocked and the message names the limit. More: Allowances.

POST/v1/transfer

Pay from the main private balance to another H081 account's h081-address: code.

Needs the tokenPlan first · sends only with "confirm": true#

In plain words: moves money from your private balance straight into another H081 account, without it ever leaving the bank. The amount and the recipient stay hidden. The chain only sees that your agent wallet used the bank.

FieldTypeWhat it is
usdg or amountstringHow much.
toAddressCode (required)stringThe recipient's h081-address: code. Your own code is refused (a transfer to yourself does nothing).
confirmbooleantrue sends it.
macOS / Linux (bash)
# 1 · the plan (nothing is sent)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"usdg":"1.5","toAddressCode":"h081-address:…"}' http://127.0.0.1:8081/v1/transfer

# 2 · send it
KEY=tr-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d '{"usdg":"1.5","toAddressCode":"h081-address:…","confirm":true}' \
  http://127.0.0.1:8081/v1/transfer
Windows PowerShell
$code = "h081-address:…"
# 1 · the plan (nothing is sent)
$body = @{ usdg = "1.5"; toAddressCode = $code } | ConvertTo-Json
(Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body $body http://127.0.0.1:8081/v1/transfer).result

# 2 · send it
$key = [guid]::NewGuid().ToString()
$body = @{ usdg = "1.5"; toAddressCode = $code; confirm = $true } | ConvertTo-Json
Invoke-RestMethod -Method Post -Headers ($h + @{ "Idempotency-Key" = $key }) -ContentType application/json -Body $body http://127.0.0.1:8081/v1/transfer
Python (requests)
body = {"usdg": "1.5", "toAddressCode": "h081-address:…"}
print(call("POST", "/transfer", body)["result"]["warnings"])       # the plan: nothing is sent

sent = call("POST", "/transfer", {**body, "confirm": True}, key=str(uuid.uuid4()))
print(sent["result"]["signature"], sent["result"]["shieldedUsdg"])
TypeScript
const body = { usdg: '1.5', toAddressCode: 'h081-address:…' };
console.log((await call('POST', '/transfer', body)).result.warnings);   // nothing is sent

const sent = await call('POST', '/transfer', { ...body, confirm: true }, randomUUID());
console.log(sent.result.signature, sent.result.shieldedUsdg);
response with confirm (example)
{ "ok": true, "network": "mainnet-beta", "status": "sent",
  "result": { "signature": "…", "amount": "1500000", "amountUsdg": "1.5", "to": "shielded",
              "via": "direct", "shielded": "1500000", "shieldedUsdg": "1.5" },
  "requestId": "req_…" }

A code that fails its checksum, is the wrong kind, or was made for another network is refused with 400 invalid_code before anything else happens.

Budgets: give and take back

In plain words: a budget (allowance) lets a sub-account of your agent, or another agent, spend some of your money with limits you set: a total, a daily cap, a cap per payment, an end date. You can take back what is left at any time. The full guide: Allowances.

POST/v1/allowances

Give a sub-account or another agent a budget with limits.

Needs the tokenPlan first · sends only with "confirm": true#

FieldTypeWhat it is
budget (required)stringThe total it may ever spend, decimal USDG.
dailyCapstringMost it may pay per day.
maxPerTxstringMost per payment.
daysnumberHow long it lasts, up to 3650. Default: no end date.
unlockDaysnumberUnlock the budget little by little over this many days (no more than days). Default: all at once.
agentIndexinteger 0 to 15Give it to this sub-account of your agent. Default: the first free one.
toAddressCodestringGive it to another agent instead: its h081-address: code.
confirmbooleantrue sends it.
macOS / Linux (bash)
# 1 · the plan: 20 USDG budget for sub-account 0, at most 5 a day and 2 per payment, for 30 days
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"budget":"20","dailyCap":"5","maxPerTx":"2","days":30,"agentIndex":0}' \
  http://127.0.0.1:8081/v1/allowances

# 2 · send it
KEY=allow-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"budget":"20","dailyCap":"5","maxPerTx":"2","days":30,"agentIndex":0,"confirm":true}' \
  http://127.0.0.1:8081/v1/allowances
Windows PowerShell
# 1 · the plan (nothing is sent)
$rules = @{ budget = "20"; dailyCap = "5"; maxPerTx = "2"; days = 30; agentIndex = 0 }
(Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body ($rules | ConvertTo-Json) http://127.0.0.1:8081/v1/allowances).result

# 2 · send it
$key = [guid]::NewGuid().ToString()
$rules.confirm = $true
Invoke-RestMethod -Method Post -Headers ($h + @{ "Idempotency-Key" = $key }) -ContentType application/json -Body ($rules | ConvertTo-Json) http://127.0.0.1:8081/v1/allowances
Python (requests)
rules = {"budget": "20", "dailyCap": "5", "maxPerTx": "2", "days": 30, "agentIndex": 0}
print(call("POST", "/allowances", rules)["result"])                # the plan: nothing is sent

sent = call("POST", "/allowances", {**rules, "confirm": True}, key=str(uuid.uuid4()))
print("budget id:", sent["result"]["allowanceId"])
TypeScript
const rules = { budget: '20', dailyCap: '5', maxPerTx: '2', days: 30, agentIndex: 0 };
console.log((await call('POST', '/allowances', rules)).result);   // the plan: nothing is sent

const sent = await call('POST', '/allowances', { ...rules, confirm: true }, randomUUID());
console.log('budget id:', sent.result.allowanceId);
response with confirm (example, shortened)
{ "ok": true, "network": "mainnet-beta", "status": "sent",
  "result": { "sent": true, "signature": "…", "allowanceId": "18340563…", "rules": { … }, … },
  "requestId": "req_…" }

Needs about 0.1 SOL for this call (keep 0.15 SOL in the agent wallet) and the grant proof file (about 51 MB, once). The plan's details shows the exact rules. The chain shows that your agent wallet made a grant, not the amount, the rules or the recipient.

POST/v1/allowances/{id}/reclaim

Take back what is left of a budget you gave.

Needs the tokenPlan first · sends only with "confirm": true#

Put the budget's id in the path, in place of {id}: the current id from GET /v1/balance, where it shows "reclaimable": true. The id changes after every payment from it, so read it fresh. The body takes only confirm; send {} for the plan.

macOS / Linux (bash)
# the budget's current id, from GET /v1/balance (a long number)
ID="<budget id>"

# 1 · the plan (nothing is sent)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{}' http://127.0.0.1:8081/v1/allowances/$ID/reclaim

# 2 · take it back
KEY=reclaim-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d '{"confirm":true}' http://127.0.0.1:8081/v1/allowances/$ID/reclaim
Windows PowerShell
$id = "<budget id>"     # its current id, from GET /v1/balance
# 1 · the plan (nothing is sent)
(Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body '{}' "http://127.0.0.1:8081/v1/allowances/$id/reclaim").result

# 2 · take it back
$key = [guid]::NewGuid().ToString()
Invoke-RestMethod -Method Post -Headers ($h + @{ "Idempotency-Key" = $key }) -ContentType application/json -Body '{"confirm":true}' "http://127.0.0.1:8081/v1/allowances/$id/reclaim"
Python (requests)
bid = "<budget id>"                          # its current id, from GET /v1/balance
print(call("POST", f"/allowances/{bid}/reclaim", {})["result"])   # the plan: nothing is sent

sent = call("POST", f"/allowances/{bid}/reclaim", {"confirm": True}, key=str(uuid.uuid4()))
print(sent["result"]["reclaimedUsdg"], "USDG back")
TypeScript
const id = '<budget id>';                       // its current id, from GET /v1/balance
console.log((await call('POST', `/allowances/${id}/reclaim`, {})).result);   // nothing is sent

const sent = await call('POST', `/allowances/${id}/reclaim`, { confirm: true }, randomUUID());
console.log(sent.result.reclaimedUsdg, 'USDG back');
response with confirm (example, shortened)
{ "ok": true, "network": "mainnet-beta", "status": "sent",
  "result": { "sent": true, "signature": "…", "allowanceId": "18340563…", "reclaimed": "14000000",
              "reclaimedUsdg": "14", "lenderShare": "0", "shieldedUsdg": "15.5", … },
  "requestId": "req_…" }

If the budget backs a loan, the lender's share of what is reclaimed goes to the lender (lenderShare); the rest returns to your private balance.

Loans

In plain words: a borrower pledges a budget that unlocks over time; a lender pays out a loan against it and is repaid from a share of every payment the budget makes. A lender can never receive more than the amount lent: anything beyond that goes back to the borrower's payout account (live on mainnet since 2026-11-10). The full guide: Loans.

GET/v1/loans/quote

Price a loan from a lender's terms code. Read-only.

Needs the token#

QueryWhat it is
termsCode (required)The lender's h081-loan-terms: code.
principalA loan amount to price, decimal USDG. Default: the most these terms back.
curl
curl -s -G -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "termsCode=h081-loan-terms:…" --data-urlencode "principal=10" \
  http://127.0.0.1:8081/v1/loans/quote
response (shape)
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "pledgeBps": …, "minRemaining": "…", "maxPrincipal": "…", "principal": "…",
              "originationFee": "…", "originationFeeBps": …, "payout": "…", "note": "…", … },
  "requestId": "req_…" }

POST/v1/loans/borrow

Pledge a budget to a lender's terms and get the proof code to send them.

Needs the tokenPlan first · sends only with "confirm": true#

FieldTypeWhat it is
termsCode (required)stringThe lender's h081-loan-terms: code.
days (required)numberHow long the pledged budget lasts.
unlockDays (required)numberIt unlocks little by little over this many days (no more than days).
principalstringThe loan amount. The lender must fund exactly this.
budgetstringThe pledged budget. Default: sized to the principal.
dailyCap, maxPerTxstringLimits on the pledged budget.
agentIndexinteger 0 to 15Which sub-account. Default: the first free one.
confirmbooleantrue sends it.
curl
# 1 · the plan, with the quote in "details" (nothing is sent)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"termsCode":"h081-loan-terms:…","days":30,"unlockDays":30,"principal":"10"}' \
  http://127.0.0.1:8081/v1/loans/borrow

# 2 · pledge the budget and get the proof code for the lender
KEY=borrow-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"termsCode":"h081-loan-terms:…","days":30,"unlockDays":30,"principal":"10","confirm":true}' \
  http://127.0.0.1:8081/v1/loans/borrow
response with confirm (shape)
{ "ok": true, "network": "mainnet-beta", "status": "sent",
  "result": { "proofCode": "h081-loan-proof:…", "loanRef": "…", "allowanceId": "…", "principal": "…",
              "maxPrincipal": "…", "payoutAccount": "…", "next": "…", … },
  "requestId": "req_…" }

Nothing is borrowed until the lender funds the proof code. Needs about 0.1 SOL and the grant and stream proof files.

POST/v1/loans/lend

Lender actions: open a slot, fund a proof code, collect, claim, settle.

Needs the tokenPlan first · sends only with "confirm": true#

FieldUsed byWhat it is
action (required)allopen, fund, collect, claim or settle.
pledgeBpsopen, fundopen: the share of every borrower payment routed to the slot, in basis points (100 = 1%; from 100 to 9000). fund: the least share the proof must pledge.
proofCodefundThe borrower's h081-loan-proof: code.
loanReffund, collect, claim, settleThe loan slot number. Required for collect, claim and settle; fund defaults to the slot in the proof code.
principal / principalUsdgfundThe amount you expect to lend (base units / decimal). Refused unless it is exactly what the proof backs.
amount / usdgclaimClaim at most this much (base units / decimal). Default: everything claimable now.
confirmalltrue sends it.
curl
# Each line below is a plan: nothing is sent. To send one, add "confirm":true to the body
# and an Idempotency-Key header, as in the deposit example.

# open a slot: 20% of every borrower payment goes to it
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"open","pledgeBps":2000}' http://127.0.0.1:8081/v1/loans/lend

# fund a borrower's proof code
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"fund","proofCode":"h081-loan-proof:…"}' http://127.0.0.1:8081/v1/loans/lend

# later: claim what has been repaid into slot 12
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"claim","loanRef":"12"}' http://127.0.0.1:8081/v1/loans/lend

# then send the one you want, e.g. the fund
KEY=fund-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"action":"fund","proofCode":"h081-loan-proof:…","confirm":true}' http://127.0.0.1:8081/v1/loans/lend

collect takes shares already routed to the slot; settle closes the books. "action": "status" is refused here: use GET /v1/loans/lent. close answers 501 not_available in this version. Every lender action is sent by the agent key, so the agent wallet is public as the lender.

GET/v1/loans/lent

This agent's loan slots as a lender.

Needs the token#

QueryWhat it is
loanRefOne slot. Default: all of this agent's slots.

Each slot shows its state (open, funded, repaid, settled or closed), what is outstanding, claimable and collectable, the borrower's excess (which goes back to the borrower, never to you), and any shortfall.

curl
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8081/v1/loans/lent
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8081/v1/loans/lent?loanRef=12"
response (shape)
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "slots": [ { "loanRef": "12", "state": "funded", "principalUsdg": "…", "repaidUsdg": "…",
                           "outstandingUsdg": "…", "claimableUsdg": "…", "collectableUsdg": "…",
                           "borrowerExcessUsdg": "…", "shortfall": "…", … } ] },
  "requestId": "req_…" }

Staking H081

In plain words: relayed withdrawals above 100 USDG need some H081 staked by the agent wallet. Staking names the agent wallet to the relayer. Unstaking has a 30-day cooldown.

GET/v1/stake

The agent wallet's H081 stake.

Needs the token#

Amounts are in H081 base units (6 decimals). pending is what is waiting out the cooldown, and readyAt is when it can be unstaked (Unix time, in seconds).

curl
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8081/v1/stake
response (shape)
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "signer": "7xKX…", "walletH081": "…", "staked": "…", "pending": "…", "readyAt": "…",
              "cooldownDays": 30, "caps": { … }, … },
  "requestId": "req_…" }

POST/v1/stake

Stake, request unstake (starts the 30-day cooldown), or unstake H081.

Needs the tokenPlan first · sends only with "confirm": true#

FieldTypeWhat it is
action (required)stringstake, request_unstake or unstake. (To look, use the GET route.)
h081stringAn H081 amount, decimal, e.g. "1000". Required for stake and request_unstake; unstake takes no amount.
confirmbooleantrue sends it.
curl
# the plan for staking 1000 H081 (nothing is sent)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"stake","h081":"1000"}' http://127.0.0.1:8081/v1/stake

# send it: "confirm": true and an Idempotency-Key
KEY=stake-$(date +%s)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"action":"stake","h081":"1000","confirm":true}' http://127.0.0.1:8081/v1/stake

# later, the plan for starting the 30-day cooldown before unstaking (nothing is sent)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"request_unstake","h081":"1000"}' http://127.0.0.1:8081/v1/stake
response without confirm: the plan
{ "ok": true, "network": "mainnet-beta", "status": "plan",
  "result": { "op": "stake", "fee": "0", "solNeeded": "0.01", "circuit": null, "download": "0 MB",
              "publicOnChain": ["agent wallet", "H081 amount"], "sentBy": "agent key", "warnings": [],
              "details": { "action": "stake", "h081": "1000" },
              "next": "Nothing was sent. Send it again with confirm: true (CLI: --yes) to do it." },
  "requestId": "req_…" }

Checking codes

POST/v1/codes/check

Check any h081-* code: its kind, its network, and whether it is valid.

Needs the token#

In plain words: before you send money to a code someone gave you, this checks it was copied correctly and is for the right network. It works offline and sends nothing, so it needs no Idempotency-Key.

FieldTypeWhat it is
code (required)stringAn h081-address:, h081-loan-terms:, h081-loan-proof: or h081-key: code.
curl
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"code":"h081-address:…"}' http://127.0.0.1:8081/v1/codes/check
response · valid
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "kind": "address", "network": "mainnet-beta", "valid": true },
  "requestId": "req_…" }
response · not a code
{ "ok": true, "network": "mainnet-beta", "status": "done",
  "result": { "kind": null, "network": null, "valid": false,
              "reason": "not an H081 code (it should start with \"h081-\")" },
  "requestId": "req_…" }

kind is address, key, loan-terms or loan-proof. An h081-key: code is the agent's secret key: check it only here, on your own computer, and never paste it anywhere else.

Live events

GET/v1/events

A stream of live updates (server-sent events): balance changes, budget changes, finished and failed sends.

Needs the token#

In plain words: instead of asking "has anything changed?" over and over, your program keeps one connection open and the server tells it when something happens.

EventDataWhen
balance.changedshielded, wallet, deltaShieldedThe private or wallet balance changed.
allowance.changedid, remainingA budget changed, or is gone ("remaining": "0").
op.completedidempotencyKey, kind, signatureA confirmed write from this server went out.
op.failedidempotencyKey, kind, codeA confirmed write from this server failed.
  • Every event's data also has time. Event ids count up.
  • While at least one listener is connected, the server re-reads the chain every --poll seconds (default 20) and sends what changed. With no listener, it does not poll.
  • After a dropped connection, send the last id you saw as the Last-Event-ID header to get what you missed (the last 1,000 events are kept in memory).
  • A : ping comment arrives every 15 seconds to keep the connection open. Ignore it.
macOS / Linux (bash)
curl -N -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8081/v1/events
Python (requests) · reconnects and resumes
# pip install requests
import json, pathlib, requests

TOKEN = (pathlib.Path.home() / ".h081" / "api-token").read_text().strip()
URL = "http://127.0.0.1:8081/v1/events"
last_id = None                       # sent back as Last-Event-ID to resume after a drop

while True:
    headers = {"Authorization": "Bearer " + TOKEN}
    if last_id:
        headers["Last-Event-ID"] = last_id
    try:
        with requests.get(URL, headers=headers, stream=True, timeout=(5, 60)) as r:
            r.raise_for_status()
            event, data = None, None
            for line in r.iter_lines(decode_unicode=True):
                if line.startswith("id: "):
                    last_id = line[4:]
                elif line.startswith("event: "):
                    event = line[7:]
                elif line.startswith("data: "):
                    data = json.loads(line[6:])
                elif line == "" and event:            # a blank line ends one event
                    print(event, data)
                    if event == "balance.changed":
                        print("private balance is now", data["shielded"], "USDG")
                    event, data = None, None
    except requests.RequestException as e:
        print("reconnecting:", e)
TypeScript · Node.js 20+
// Node.js 20+. A browser EventSource cannot send the Authorization header, so this reads the stream with fetch.
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

const token = readFileSync(join(homedir(), '.h081', 'api-token'), 'utf8').trim();
const res = await fetch('http://127.0.0.1:8081/v1/events', { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok || !res.body) throw new Error(`events: HTTP ${res.status}`);

const decoder = new TextDecoder();
let buf = '';
for await (const chunk of res.body) {
  buf += decoder.decode(chunk, { stream: true });
  let end;
  while ((end = buf.indexOf('\n\n')) >= 0) {          // a blank line ends one event
    const block = buf.slice(0, end);
    buf = buf.slice(end + 2);
    const ev: Record<string, string> = {};
    for (const line of block.split('\n')) {
      const i = line.indexOf(': ');
      if (i > 0) ev[line.slice(0, i)] = line.slice(i + 2);   // skips ": ping" comments
    }
    if (ev.event) console.log(ev.id, ev.event, JSON.parse(ev.data));
  }
}
what arrives (example)
retry: 3000

id: 12
event: balance.changed
data: {"shielded":"3","wallet":{"usdg":"0","sol":"0.0412"},"deltaShielded":"-2","time":"2026-11-11T09:31:02.114Z"}

id: 13
event: op.completed
data: {"idempotencyKey":"wd-1","kind":"withdraw","signature":"…","time":"2026-11-11T09:31:02.120Z"}

: ping

Errors in short

Switch on error.code. If retryable is true, wait (use Retry-After when it is there) and send the same request with the same key. Every code with its fix: Errors and fixes.

HTTPCodes
400bad_request, idempotency_key_required, invalid_code
401unauthorized
403forbidden_origin
404not_found
409in_progress (retryable), outcome_unknown, insufficient_shielded, insufficient_wallet_usdg, needs_sol, cap_per_deposit, cap_total, deposits_paused, over_operator_ceiling, needs_stake, allowance_blocked, stale_root (retryable), in_flight (retryable), already_spent
415unsupported_media_type
422idempotency_key_reused, simulation_failed, program_error
429rate_limited (retryable)
500no_key, internal
501not_available
502rpc_unreachable (retryable)
503relayer_unavailable (retryable), relayer_busy (retryable), busy (retryable), circuits_missing, circuit_hash_mismatch

The OpenAPI file

Every route, field and error code in one machine-readable file (OpenAPI 3.1). Three ways to get it:

  • On this site: /h081-agent-openapi.json.
  • From a running server: GET /v1/openapi.json (no token).
  • From the command line, no server needed:
any terminal
npx -y @h081lll/agent openapi > h081-openapi.json

Next

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.