TypeScript SDK

Use the H081 class from @h081lll/agent in your own Node.js and TypeScript code: balance, deposit, withdraw, pay, budgets and loans, with a plan before every send.

In plain words

The SDK lets a program you write use your agent's bank directly. If your agent is written in JavaScript or TypeScript (the language Node.js runs), it imports one class, H081, and calls methods such as balance(), withdraw() or pay().

There is no server to start and nothing hosted: the code runs inside your program, on your computer, with the agent key in ~/.h081/agent.json. Like the command line, every method that moves money returns a plan and sends nothing until you pass confirm: true.

Not using JavaScript? The local API gives the same bank to any language.

Install and first script

You need Node.js 20.11 or newer. The package is an ES module: use import, in a .mjs or .mts file or a project with "type": "module".

  1. Add the package to your project

    in your project folder
    npm install @h081lll/agent
  2. Create the agent key, once

    This writes the key and settings into ~/.h081. Skip it if you already did it for the command line or Claude: they all share the same key.

    any terminal
    npx -y @h081lll/agent init --rpc <your Solana RPC URL>

    Then send 0.05 SOL and a few USDG to the wallet it prints (0.15 SOL if you will give budgets or lend).

  3. Open the bank and read the balance

    first.mts
    // first.mts
    import { H081 } from '@h081lll/agent';
    
    const bank = await H081.open();          // reads ~/.h081/agent.json, ~/.h081/config.json and the environment
    console.log(bank.address());             // { wallet, addressCode }
    console.log(await bank.balance());       // { shielded, wallet: { usdg, sol }, allowances, ... }
    bank.close();
    run it (tsx runs TypeScript files directly)
    npx tsx first.mts

H081.open()

H081.open(options?) loads the agent and returns an H081 object. It works offline: the Solana RPC is reached only when a method needs the chain. If there is no key it throws, so run init first.

Where each setting comes from, first match wins:

SettingRead from
The agent keyH081_SOLANA_KEYPAIR (a file path), then H081_SOLANA_KEY (the key itself), then ~/.h081/agent.json
RPC, network, portThe environment (H081_RPC_URL, H081_CLUSTER, H081_API_PORT), then ~/.h081/config.json, then the mainnet defaults
The data folderThe home option, then H081_HOME, then ~/.h081

Options

OptionDefaultWhat it does
homeH081_HOME or ~/.h081The data folder. One per agent.
envprocess.envThe environment to read settings from.
cluster'mainnet-beta''mainnet-beta', or 'localnet' for tests.
logstderrA function that gets each log line. Never a key, a token or a full RPC URL.
maxSendsPerMinH081_API_MAX_SENDS_PER_MIN or 30Confirmed sends allowed per minute.
retryDelayMs1000The wait before the one automatic retry when the relayer is busy or rate-limited.
pollSeconds20How often events re-read the balance while someone listens.
TypeScript
import { H081 } from '@h081lll/agent';

// the defaults: ~/.h081 and process.env
const bank = await H081.open();

// a second agent with its own folder, quieter logs, and a lower send limit
const other = await H081.open({
  home: '/home/you/agent-two',
  log: () => {},
  maxSendsPerMin: 5,
});

Plan first, then confirm

Every method that moves money takes the same two options at the end, and returns an Outcome:

confirm
Leave it out and you get status: 'plan': what would happen, with nothing proven, signed or sent. Pass true to send.
idempotencyKey
A name for this one send, so a retry never pays twice (see below). Default: a new random key.
TypeScript
const to = '<a wallet that never touched the agent>';

// 1. Without confirm: the plan. Nothing is proven, signed or sent.
const plan = await bank.withdraw('2', { to });
if (plan.status === 'plan') {
  console.log(plan.result.fee, plan.result.payout, plan.result.warnings);
}

// 2. With confirm: true it is sent.
const sent = await bank.withdraw('2', { to, confirm: true, idempotencyKey: 'wd-2026-11-11-a' });
console.log(sent.status, sent.result);   // 'sent', { signature, receivedUsdg, shieldedUsdg, ... }
what the plan looks like (an illustration; your numbers differ)
{
  status: 'plan',
  result: {
    op: 'withdraw', usdg: '2', to: '<a wallet that never touched the agent>',
    fee: '0.05', payout: '1.95', solNeeded: '0', circuit: 'transact', download: '15 MB',
    publicOnChain: [ 'recipient', 'amount received' ], sentBy: 'relayer',
    warnings: [ 'If <a wallet that never touched the agent> has no USDG account yet, the relayer adds … USDG to create it.' ],
    next: 'Nothing was sent. Send it again with confirm: true (CLI: --yes) to do it.'
  }
}
statusWhenresult
'plan'No confirm: trueA Plan: op, usdg, to, fee, payout (what arrives), solNeeded, circuit, download, publicOnChain, sentBy ('relayer' or 'agent key'), warnings, details, next
'sent'It was sentThe chain result, with signature. Also idempotencyKey, and replayed: true when it was sent earlier with the same key.
'done'A read through a write method (lend('status'), stake('view'))The answer

Amounts are USDG as decimal strings: '5', '2.5', '0.25'.

Retry safely with idempotencyKey

A network can fail halfway. If you are not sure a payment went out, call again with the same key and the same arguments: if it went out, you get the first result back and nothing is sent twice. Records live in ~/.h081/ops.jsonl for 7 days and are shared with the command line and the local API on this computer.

TypeScript
const key = 'invoice-2026-11-0042';      // one key per real-world payment

const first = await bank.withdraw('2', { to, confirm: true, idempotencyKey: key });
// the process crashes, or you are not sure it went out: call again with the SAME key and the SAME arguments
const again = await bank.withdraw('2', { to, confirm: true, idempotencyKey: key });
console.log(again.status === 'sent' && again.replayed);   // true: the first result, nothing sent twice
Same key again, and…You get
the first call finishedIts result, with replayed: true. Nothing is sent.
the first call is still runningThe error in_progress. Wait and call again.
different argumentsThe error idempotency_key_reused. Use a new key for a new payment.
the process stopped mid-sendThe error outcome_unknown: it may have gone out. Check balance({ fresh: true }) and history() before you try again with a new key.
it failed with a retryable error and nothing was sentThe key is free again: the same call may run with it.

A key is 1 to 128 characters of A-Z a-z 0-9 _ . : -. Confirmed sends go one at a time through a queue; more than 8 waiting gives the error busy.

Reading: balance, history, fees

These never send anything and never need confirm.

address()

The agent's public wallet and its h081-address: code. Offline, not async.

TypeScript
const { wallet, addressCode } = bank.address();
console.log('Fund this wallet:', wallet);
console.log('Others pay you privately with:', addressCode);

checkCode(code)

Checks any h081- code before you use it: { kind, network, valid, reason? }. Offline, not async.

TypeScript
const c = bank.checkCode('<a code someone sent you>');
if (!c.valid) console.log('Do not use it:', c.reason);
else console.log(c.kind, c.network);     // 'address' 'mainnet-beta'

status() and info()

status() says where things stand without touching the network. info() adds the protocol facts read from the chain.

TypeScript
console.log(bank.status());
// { network, agent, keySource, rpcHost, relayer, circuits: { present, missing }, queue: { depth }, fundingNeeded, ... }
console.log(await bank.info());          // protocol facts, deposit caps, relayer terms, SOL needed per use

balance(options?)

The private balance, the wallet's USDG and SOL, the deposit caps and every budget with its id. An answer up to 15 seconds old may be reused; fresh: true reads the chain now.

TypeScript
const b = await bank.balance({ fresh: true });
console.log(b.shielded + ' USDG private, ' + b.wallet.usdg + ' USDG and ' + b.wallet.sol + ' SOL in the wallet');
for (const a of b.allowances) console.log(a.id, a.remaining, a.holder, a.dailyCap, a.reclaimable);
what it returns (an illustration)
{
  shielded: '3',
  wallet: { usdg: '0', sol: '0.04' },
  caps: { perDeposit: '1000', total: '10000', inVault: '…', paused: false },
  allowances: [
    { id: '12345', remaining: '4.5', holder: 'sub-account 1', agentIndex: 1, dailyCap: '1', maxPerTx: '0.25',
      expires: '2026-12-11T09:00:00.000Z', reclaimable: true, pledged: false }
  ],
  addressCode: 'h081-address:…',
  syncedAt: '2026-11-11T09:00:00.000Z'
}

history(options?)

What this computer sent, newest first, from ~/.h081/ops.jsonl. limit is 1 to 200 (default 50); pass the returned next as after for the next page. Not async.

TypeScript
const page = bank.history({ limit: 20 });   // not async: it reads ~/.h081/ops.jsonl
for (const i of page.items) console.log(i.time, i.kind, i.usdg, i.state, i.signature ?? i.error);
if (page.next) console.log(bank.history({ after: page.next }).items.length, 'more');

withdrawFee(usdg)

What a withdrawal would cost: fee, payout, feeBps, minFee, freeTierMax, needsStake, relayer and, when the relayer charges for a new USDG account, createAccountFee. No proof is made.

TypeScript
const q = await bank.withdrawFee('2');
console.log(q.fee, q.payout, q.needsStake);   // '0.05' '1.95' false

Moving money

Each one returns the plan unless you pass confirm: true. Every example shows the plan first.

deposit(usdg, options?)

Wallet USDG into the private balance. Public: the chain shows the agent wallet and the amount.

TypeScript
console.log(await bank.deposit('5'));                    // the plan
await bank.deposit('5', { confirm: true, idempotencyKey: 'dep-1' });

withdraw(usdg, { to, direct? })

Out of the bank to any wallet, sent by the relayer so the agent wallet is not shown as the sender. to is required: a wallet address, or 'self' for the agent wallet. Withdraw to a fresh wallet, never the one that deposited.

TypeScript
const to = '<a wallet that never touched the agent>';
console.log(await bank.withdraw('2', { to }));                                 // the plan
await bank.withdraw('2', { to, confirm: true, idempotencyKey: 'wd-1' });       // through the relayer
// direct: true has the agent key send it: no fee, but the chain shows the agent wallet as the sender

pay(usdg, { to | toAddressCode, allowanceId?, agentIndex?, direct?, allowPledged? })

Pays from a budget this agent holds. Give exactly one recipient: toAddressCode (private: amount and recipient hidden) or to (a wallet, public payout through the relayer). agentIndex pays from a budget your sub-account holds; allowanceId picks one budget; otherwise one whose limits allow the amount is picked. A budget pledged to a loan is used only with allowPledged: true.

TypeScript
const shop = '<their h081-address: code>';
console.log(await bank.pay('0.5', { toAddressCode: shop, agentIndex: 1 }));    // the plan
await bank.pay('0.5', { toAddressCode: shop, agentIndex: 1, confirm: true, idempotencyKey: 'order-77' });

// a public payout to a wallet instead, from a budget picked by id: the plan first, then the same call with confirm: true
console.log(await bank.pay('0.5', { to: '<their wallet>', allowanceId: '12345' }));

transfer(usdg, { toAddressCode })

From the main private balance to another H081 account's h081-address: code. Amount and recipient stay hidden.

TypeScript
const friend = '<their h081-address: code>';
console.log(await bank.transfer('1', { toAddressCode: friend }));              // the plan
await bank.transfer('1', { toAddressCode: friend, confirm: true, idempotencyKey: 'tr-1' });

allow(budget, { dailyCap?, maxPerTx?, days?, unlockDays?, agentIndex?, toAddressCode? })

Gives a budget to your sub-account agentIndex (default: the first free one) or to another agent by toAddressCode. The limits are checked on every payment. days ends it; unlockDays releases it bit by bit. The plan's details shows the exact rules. More on Allowances.

TypeScript
const rules = { dailyCap: '1', maxPerTx: '0.25', days: 30, agentIndex: 1 };
console.log(await bank.allow('5', rules));                                     // the plan, with the exact rules
await bank.allow('5', { ...rules, confirm: true, idempotencyKey: 'budget-research-1' });

// to another agent instead of a sub-account: the plan first, then the same call with confirm: true
console.log(await bank.allow('2', { toAddressCode: '<their h081-address: code>', days: 7 }));

reclaim(allowanceId, options?)

Takes back what is left of a budget you gave. The id changes after every payment, so read balance() just before.

TypeScript
const b = await bank.balance({ fresh: true });         // ids change after every payment
for (const a of b.allowances.filter((x) => x.reclaimable)) {
  console.log(await bank.reclaim(a.id));                   // the plan: how much comes back
  await bank.reclaim(a.id, { confirm: true });
}

Loans and staking

Borrow against a budget, or lend to one. A lender can never receive more than the principal: anything paid beyond it goes back to the borrower's payout account. Step by step for both sides on Loans.

loanQuote(termsCode, { principal? }) and borrow(termsCode, { days, unlockDays, principal?, budget?, dailyCap?, maxPerTx?, agentIndex? })

The borrower's side. loanQuote prices a loan from the lender's h081-loan-terms: code and sends nothing. borrow pledges a budget and returns an h081-loan-proof: code for the lender. Nothing is borrowed until the lender funds it.

TypeScript
const terms = '<the lender h081-loan-terms: code>';
console.log(await bank.loanQuote(terms, { principal: '10' }));      // most you can borrow, the 0.25% fee, the payout

const opts = { days: 30, unlockDays: 30, principal: '10' };
console.log(await bank.borrow(terms, opts));                        // the plan
const done = await bank.borrow(terms, { ...opts, confirm: true });  // pledges a budget
console.log(done.result);   // includes proofCode: the h081-loan-proof: code to hand to the lender

lend(action, fields, options?) and lendStatus(loanRef?)

The lender's side. action is 'open' (fields: pledgeBps, 100 to 9000), 'fund' (proofCode), 'claim' (loanRef, optional usdg), 'collect' or 'settle' (loanRef). The agent key sends every lender action, so the agent wallet is public as the lender.

TypeScript
console.log(await bank.lend('open', { pledgeBps: 2000 }));                       // the plan
const open = await bank.lend('open', { pledgeBps: 2000 }, { confirm: true });    // result: the terms code to hand out
console.log(await bank.lend('fund', { proofCode: '<the borrower h081-loan-proof: code>' }));   // the plan
await bank.lend('fund', { proofCode: '<the borrower h081-loan-proof: code>' }, { confirm: true, idempotencyKey: 'fund-1' });
console.log(await bank.lendStatus('3'));                                         // funded, repaid, owed, claimable
console.log(await bank.lend('claim', { loanRef: '3' }));                         // the plan
await bank.lend('claim', { loanRef: '3' }, { confirm: true });                   // never more than the loan still owes

stakeView() and stake(action, { h081? })

Stake H081 to withdraw more than 100 USDG at a time through the relayer. action is 'stake', 'request_unstake' (starts a 30-day wait) or 'unstake'.

TypeScript
console.log(await bank.stakeView());
console.log(await bank.stake('stake', { h081: '1000' }));                        // the plan
await bank.stake('stake', { h081: '1000', confirm: true });
await bank.stake('request_unstake', { h081: '1000', confirm: true });           // then wait 30 days
await bank.stake('unstake', { confirm: true });

Live updates

bank.events.subscribe(fn) calls fn for each event and returns a function that stops it. While at least one listener is subscribed, the balance is re-read every pollSeconds, and changes arrive as balance.changed and allowance.changed. Sends made through this object arrive as op.completed and op.failed. Call close() when you are done so the program can exit.

TypeScript
const stop = bank.events.subscribe((e) => {
  console.log(e.id, e.type, e.data);    // balance.changed, allowance.changed, op.completed, op.failed
});
// ... later
stop();
bank.close();

Run the local API from your own program

The same API that npx -y @h081lll/agent serve starts, inside your process. It listens on 127.0.0.1 only.

TypeScript
import { H081, readToken, startServer } from '@h081lll/agent';

const bank = await H081.open();
const token = readToken(bank.paths.token);          // ~/.h081/api-token, written by init
if (!token) throw new Error('no API token yet: run h081 token once');
const srv = await startServer({ h081: bank, token });
console.log('local API on', srv.url);              // http://127.0.0.1:8081/v1
// ... later
await srv.close();
bank.close();

Errors

When something goes wrong, the method throws. Nothing was sent unless the error says it may have been (outcome_unknown). Pass any error to classify(e) to get the same stable fields the local API returns:

code
A stable name such as needs_sol, insufficient_shielded, allowance_blocked or relayer_busy. All of them are on Errors and fixes.
retryable
true when the same call may simply be tried again later.
message, details, status
A sentence you can show, extra facts (for example details.needSol), and the HTTP status the API would use.
TypeScript
import { classify } from '@h081lll/agent';

try {
  await bank.withdraw('2', { to, confirm: true, idempotencyKey: 'wd-7' });
} catch (e) {
  const err = classify(e);   // { code, status, retryable, message, details }
  if (err.code === 'needs_sol') console.log('Send a little SOL to', bank.address().wallet);
  else if (err.code === 'outcome_unknown') console.log('It may have gone out: check balance({ fresh: true }) before a new key');
  else if (err.retryable) console.log('Nothing was sent; try again later with the same key:', err.message);
  else throw e;
}

Errors the SDK raises itself (a bad amount, a reused key, an invalid code) are CodedError objects with code and details already set. Errors from the chain or the relayer (for example allowance_blocked) are plain errors, so always use classify(e) to read the code. ERROR_CODES lists every code with its status and whether it is retryable.

TypeScript
import { classify, CodedError, ERROR_CODES } from '@h081lll/agent';

try {
  await bank.pay('50', { toAddressCode: shop, agentIndex: 1, confirm: true });
} catch (e) {
  if (e instanceof CodedError) console.log('refused by the SDK:', e.code, e.details);   // e.g. invalid_code
  const err = classify(e);                           // works for every error, CodedError or not
  if (err.code === 'allowance_blocked') {
    console.log(err.message);                        // names the limit that stopped it
    console.log(ERROR_CODES[err.code].retryable);    // false
  }
}

Complete example: a research agent with a daily budget

A small, real program. Your main agent gives a research agent (sub-account 1) a budget of 5 USDG for 30 days, at most 1 USDG a day and 0.25 USDG per purchase. The research agent then buys papers from a data provider, paying privately to its h081-address: code, until the daily cap stops it. Run as it is, it only prints plans; with SEND=1 it sends.

  1. Save the script

    Put the data provider's h081-address: code in PROVIDER.

    research-agent.mts
    // research-agent.mts: a research agent with a daily budget.
    // It gets 5 USDG for 30 days, at most 1 USDG a day and 0.25 USDG per purchase, in sub-account 1,
    // and buys papers from a data provider privately, by its h081-address: code.
    // Run:  npx tsx research-agent.mts          (plans only)
    //       SEND=1 npx tsx research-agent.mts   (sends)
    import { H081, classify } from '@h081lll/agent';
    
    const SEND = process.env.SEND === '1';
    const PROVIDER = '<the data provider h081-address: code>';
    const SUB = 1;
    const PRICE = '0.25';
    
    const bank = await H081.open();
    
    // 1. The budget, once. The fixed key means a second run never grants it twice.
    const rules = { dailyCap: '1', maxPerTx: PRICE, days: 30, agentIndex: SUB };
    const has = (await bank.balance({ fresh: true })).allowances.some((a) => a.agentIndex === SUB && a.remaining !== '0');
    if (!has) {
      const grant = await bank.allow('5', { ...rules, confirm: SEND, idempotencyKey: 'research-budget-2026-11' });
      console.log(grant.status === 'plan' ? 'Budget plan (nothing sent):' : 'Budget granted:', grant.result);
    }
    
    // 2. Buy what the research needs until today's cap says stop. One key per paper per day: a re-run the same day
    //    never pays twice, and a paper the cap refused today can be tried again tomorrow.
    const today = new Date().toISOString().slice(0, 10);
    //    history() keeps 7 days: for a longer project, keep your own list of what was bought.
    const bought = new Set(bank.history({ limit: 200 }).items.filter((i) => i.state === 'done').map((i) => i.id.split(':')[0]));
    
    async function buy(paper: string): Promise<boolean> {
      if (bought.has('paper-' + paper)) return true;               // paid on an earlier day
      try {
        const out = await bank.pay(PRICE, { toAddressCode: PROVIDER, agentIndex: SUB, confirm: SEND, idempotencyKey: 'paper-' + paper + ':' + today });
        if (out.status === 'plan') {
          console.log(paper + ': would pay ' + PRICE + ' USDG (nothing sent)', out.result.warnings);
          return true;
        }
        const r = out.result as { signature: string; remainingUsdg?: string };
        console.log(paper + ': paid' + (out.replayed ? ' earlier' : '') + ', ' + r.remainingUsdg + ' USDG left (' + r.signature + ')');
        return true;
      } catch (e) {
        const err = classify(e);
        if (err.code === 'allowance_blocked') {
          console.log(paper + ': not today. ' + err.message);   // the daily cap, the per-payment cap or the budget is used up
          return false;
        }
        throw e;
      }
    }
    
    for (const paper of ['2611-0042', '2611-0057', '2611-0101', '2611-0133', '2611-0150']) {
      if (!(await buy(paper))) break;
    }
    
    console.log('Private balance:', (await bank.balance({ fresh: true })).shielded, 'USDG');
    bank.close();
  2. Run it without sending

    any terminal
    npx tsx research-agent.mts

    Read the plans it prints: the budget's rules, and each payment's warnings.

  3. Run it for real

    macOS / Linux
    SEND=1 npx tsx research-agent.mts
    Windows PowerShell
    $env:SEND = "1"; npx tsx research-agent.mts
    what it prints (an illustration; your numbers differ)
    Budget granted: { sent: true, signature: '4tN…', agentIndex: 1, allowanceId: '…', rules: { … }, … }
    2611-0042: paid, 4.75 USDG left (3pK…)
    2611-0057: paid, 4.5 USDG left (5Wq…)
    2611-0101: paid, 4.25 USDG left (2Bd…)
    2611-0133: paid, 4 USDG left (67m…)
    2611-0150: not today. … (the daily cap)
    Private balance: 0 USDG

    Run it again tomorrow: the budget is not granted twice (it checks the balance first, and the grant has a fixed key), papers already bought are skipped (it reads history(), which keeps 7 days), and the daily cap allows 1 USDG more.

When the project ends, take back what is left:

TypeScript
// When the project ends: take back what is left of the research budget.
const b = await bank.balance({ fresh: true });
for (const a of b.allowances.filter((x) => x.agentIndex === SUB && x.reclaimable)) {
  console.log(await bank.reclaim(a.id));                           // the plan
  await bank.reclaim(a.id, { confirm: true, idempotencyKey: 'research-reclaim-' + a.id });
}

The same call from other tools

The SDK, the command line and the local API share one engine, one key and the same errors. Here is the same withdrawal plan four ways; none of them sends anything.

macOS / Linux (bash), with serve running
TOKEN=$(cat ~/.h081/api-token)
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
Windows PowerShell, with serve running
$h = @{ Authorization = "Bearer $(Get-Content $HOME\.h081\api-token)" }
Invoke-RestMethod -Method Post -Headers $h -ContentType application/json -Body '{"usdg":"2","to":"<fresh wallet>"}' http://127.0.0.1:8081/v1/withdraw
Python (requests), with serve running
import pathlib, requests

token = (pathlib.Path.home() / ".h081" / "api-token").read_text().strip()
r = requests.post("http://127.0.0.1:8081/v1/withdraw",
                  headers={"Authorization": "Bearer " + token},
                  json={"usdg": "2", "to": "<fresh wallet>"})
print(r.json()["result"])   # the plan: nothing is sent
TypeScript (SDK, no server)
import { H081 } from '@h081lll/agent';

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

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.