On this page
- Before you start
- Create the instance
- Errors you may see
- Turn an invoice into facts
- What the tests pass
- Choose who decides
- Your own rules
- Run the loop
- The cycle in code order
- One call at a time
- Wire it into your agent
- Handle the outcome
- Find a spend by its ref
- Refs and retries
- One ledger writer per vault
- Check your work
- Common mistakes
- Not available yet
- Related pages
Let an agent pay
First payment? Start with the developer quickstart: this page turns your agent's invoice handling into a loop that pays each invoice at most once through process().
Your agent's invoice handling becomes a loop that pays through process(): each invoice is paid at most once (each decision's spend is sent once), one ledger writer keeps each vault's history, and no unknown outcome is ever paid twice. You need the SDK, from npm or built from a clone (Install), and a testnet vault whose owner has signed the steps below. The developer quickstart runs one payment end to end; this page covers what a loop that keeps paying has to get right.
Everything here runs on NEAR testnet with test tokens. On mainnet only one small vault runs, v0.probarion.near, and no other can be created yet: see Testnet and mainnet. This loop is Preview: no run of a loop of your own like this page's is recorded. The console's Payables runs the same process() on the testnet demo vault, and on 2026-11-12 two of its cycles each paid an invoice of 2.00 test USDC on its own, with an attested model (the record (opens in a new tab), the second (opens in a new tab)). The outputs below come from the code and the SDK's own tests, which run against a fake chain.
Key idea
The agent key can call only spend, and a spend names no payee: the vault pays the payee the mandate names. See Why spend has no payee.
Before you start#
Make the agent key and the ledger key on your server with agent keygen --out <file>. Each run saves the pair to a new file as { public_key, secret_key } and prints only the public key. Send both public keys, and the purchase order or contract the agent pays against, to the vault owner. The owner then makes sure of these:
- The vault holds test USDC. On a testnet vault's Top-up tab (
/console/<vault>/topup), the "Add test USDC" button mints 25 test USDC into the vault with no wallet. The Probarion team's mint-only key signs it, once a UTC day per vault and for up to 3 vaults per signed-in account a day. Without a faucet key the tab shows this instead: "Need test USDC? Send @probarionhq a direct message on X with your vault's name." - Your agent key is on the vault.
agent add --pk <agent public key>prints theadd_agentcall. The vault adds a function-call key to itself that can call onlyspendand never attach a deposit. A vault holds at most 16 agent keys, and a key cannot be both an agent key and the ledger key. - Your ledger key is the vault's ledger key.
ledger set-key --pk <ledger public key>prints theset_ledger_keycall. It replaces any previous ledger key, and the key must not already be an agent key or the current ledger key. Do not run it on a vault that runs the console's hosted Payables: it would replace the ledger key Payables signs with. Give your agent a vault of its own, as One ledger writer per vault explains. - A mandate names your agent key and your purpose hash. The owner signs it in the console, whose mandate form lists every agent key on the vault and hashes the purchase order file in the browser. Or the owner prints it with
mandate create: its--agent-pkis your agent public key, its--purposeis the purchase order file, and it also requires--payee,--max-per,--max-total,--uses,--expires,--approval-aboveand--label. See Mandates.
# On your server: each file keeps its secret, and keygen never overwrites one.
node packages/cli/dist/bin.js agent keygen --out ../agent-key.json
node packages/cli/dist/bin.js agent keygen --out ../ledger-key.json
# For the owner. Illustrative: replace <your-vault>, <owner> and each ed25519:... with real values.
node packages/cli/dist/bin.js --network testnet agent add --vault <your-vault>.vaults.probarion.testnet \
--pk ed25519:... --sign-as <owner>.testnet --out-dir probarion-tx
node packages/cli/dist/bin.js --network testnet ledger set-key --vault <your-vault>.vaults.probarion.testnet \
--pk ed25519:... --sign-as <owner>.testnet --out-dir probarion-txThe CLI never signs. Each owner command writes its arguments to a file in probarion-tx/ and prints the near-cli-rs command under this line:
OWNER SIGNS: probarion never signs owner or approver transactions. Review the arguments, then sign with your own wallet or near-cli-rs:Owner signs
The owner reviews each printed command and its arguments file, then signs it. agent add and ledger set-key each use 30 Tgas and attach exactly 1 yoctoNEAR. They give the key a gas allowance of 1 NEAR and 0.25 NEAR unless --allowance says otherwise, and the vault refuses an allowance of 0. mandate create attaches a storage deposit, 0.05 NEAR unless --deposit says otherwise, and the vault refunds the excess.
The console's Keys tab makes its own keys on Probarion's server, for hosted Payables. Your agent brings its own keys, so they go on the vault through the CLI.
The console's Agent (MCP) tab (/console/<vault>/agent) is the other way, built for the MCP server and tried on testnet on 2026-11-11, with the two keys npx -y @probarion/mcp@0.1.1 init made on the owner's machine. The owner pastes the two public keys there, and the page refuses a pasted secret key before anything is sent. The owner signs add_agent and set_ledger_key in their wallet, and the tab gives the settings for Claude Code, Claude Desktop, Cursor and Codex. It withholds the ledger key from a vault whose ledger another writer keeps.
The MCP server @probarion/mcp is Live: it is on npm at 0.1.1, so npx -y @probarion/mcp@0.1.1 runs it, as its README (opens in a new tab) says. On 2026-11-11 a real MCP client, Claude Code, ran it from npm on a testnet vault: one spend paid, two were held until the owner approved them in the console, and the server's verify passed. On 2026-11-12 the same client asked for a fourth spend, which the vault held, and the owner rejected it in the console, so nothing was paid. The prompt gave the tool arguments, so the client read no invoice, and the decisions are rules-only (the record (opens in a new tab)). Its request_spend tool calls this same SDK's requestSpend: the client names the mandate by id, so the mandate's caps, uses, expiry and agent key, the duplicate checks and the contract's checks apply, while no invoice text is read and the purpose rule passes. None of its tools takes an address.
On the "Overview" tab, the "Your first agent payment" card offers its "Agent key and ledger key" step two ways, "Go to Keys" for hosted Payables and "Connect agent" for your own agent. It ticks the step once the chain shows an agent key and a ledger key that the console did not make. Skip "Go to Keys": a ledger key made there would replace yours, and the Keys tab asks before it does. The card goes away after the first payment. The owner quickstart covers the vault itself.
Create the instance#
new Probarion(options) makes the client for one vault.
| Option | Value | What it does |
|---|---|---|
network | "testnet" or "mainnet" | Required. The demo vaults are on testnet. On mainnet only v0.probarion.near runs, and no other mainnet vault can be created yet. |
vault | The vault account id | Required. |
agentKey | "ed25519:..." | The secret of your agent key, a function-call key on the vault account that can call only spend. Without it the instance is read-only. |
ledgerKey | "ed25519:..." | The secret of your ledger key, which can call only anchor and signs every ledger entry. |
rpcUrls | A list of RPC URLs | Tried in turn, with failover. Default: NEAR_RPC_URLS when set, for whichever network you pass, else the network's own list. |
model | null, or { provider: "near-ai-cloud", apiKey, model?, systemPrompt? } | null or absent: the code decides alone. See Choose who decides. |
ledger | A LedgerStore | Default: a file at PROBARION_LEDGER_PATH, else .probarion/ledger.jsonl. A blank variable counts as unset. |
confidenceFloor | A number from 0 to 1 | Default 0.7. A model proposal below it holds the spend. |
requestId | Text | Written into the request_id of every entry this instance writes. Without it, each cycle gets a fresh UUID. |
Your code passes both keys: the example below reads each secret_key from the file keygen wrote. The SDK does not read PROBARION_AGENT_KEY, PROBARION_LEDGER_KEY or NEARAI_API_KEY by itself, even though its error messages name them. Keep both secrets on the server and out of the repository.
Errors you may see#
Each is a ConfigError (Errors). These come from the constructor:
invalid secret key for <vault>: ...: a key string is not a valid secret. The message names the vault account, never the key.model.apiKey (NEARAI_API_KEY) is required for the model step; pass model: null to decide rules-only:modelwas set with an empty or missingapiKey.confidenceFloor must be a number from 0 to 1.
These come from process(), before it writes or sends anything:
this instance is read-only: process() and requestSpend() need agentKey (PROBARION_AGENT_KEY), the vault's function-call key for spend: noagentKeywas passed.process() and requestSpend() need ledgerKey (PROBARION_LEDGER_KEY): every ledger entry is signed with it: noledgerKeywas passed.the vault has no ledger key yet (set_ledger_key): entries signed now could never be verified: the owner'sledger set-keyhas not landed.ledgerKey is <key>, but the vault's ledger key is <key>: its entries would fail verify check a: the vault holds another ledger key, such as the console's.the agent key signs as <account>, but this vault is <vault>: function-call keys live on the vault account itself: only an injected sender (deps.agentSender) can sign as another account.
Turn an invoice into facts#
process() takes { text, facts, ref }. The SDK extracts nothing from the text: your agent reads the invoice and supplies the facts.
| Field | Rule |
|---|---|
text | The extracted invoice text, exactly as you read it. It is untrusted. |
facts.invoice_hash | hashInvoice(text): the sha256 of the text's UTF-8 bytes, 64 lowercase hex. A PDF's file hash is not the invoice hash. |
facts.amount | A decimal string in USDC base units. USDC has 6 decimals, so "25000000" is 25 USDC. "25.00" or "0" is refused by code with invoice.amount_invalid. |
facts.po_ref | null, or the purchase order reference copied verbatim from text, with no trimming, case change or normalisation. An empty string counts as null. |
facts.purpose_hash | 64 lowercase hex, or null: the sha256 of the file the mandate was signed with. It chooses the mandate. |
facts.payee_hint | null, or what the invoice claims about the payee: near_account, address, name, and chain set to base or solana. It is never the destination. A hint that contradicts the mandate's payee is refused with mandate.payee_mismatch. |
ref | Your reference for this payment: 1 to 64 bytes of UTF-8 text. The vault accepts each ref once. |
The SDK picks the mandate by purpose_hash: the lowest-id mandate with that purpose that your agent key can still use (not revoked, not expired, bound to this key, a use left, room under its total). When none qualifies it takes the lowest-id match anyway, so the refusal names the precise reason. With no match at all, the cycle is refused with mandate.none.
A malformed call throws ProbarionError with code InvalidInput (Errors) before anything is read, written or sent: an invoice_hash that is not hashInvoice(text), an amount that is not a string, a po_ref that is not in the text, a bad purpose_hash, payee_hint or ref.
What the tests pass#
The SDK's tests build their invoices like this. For a string, sha256Hex(text) is the same value as hashInvoice(text). The helper's default po_ref is PO-1001, so the text must contain PO-1001 unless the test passes another po_ref:
export function nearInvoice(
text: string,
ref: string,
facts: Partial<InvoiceFacts> = {},
): TestInvoice {
return {
text,
ref,
facts: {
invoice_hash: sha256Hex(text),
amount: "25000000",
po_ref: "PO-1001",
purpose_hash: SCENARIO_PURPOSE_NEAR,
payee_hint: { near_account: SCENARIO_NEAR_PAYEE },
...facts,
},
};
}Choose who decides#
model: null: the code rules decide alone. Each decision entry recordsdecided_by: "rules-only".- NEAR AI Cloud:
model: { provider: "near-ai-cloud", apiKey }asks a private model in a TEE for one proposal per invoice and records its attestation.modelnames a model id; without it the SDK readsNEARAI_MODEL, else picks an eligible private model from the catalog. When the model's proposal passes its attestation checks and is used, the decision entry recordsdecided_by: "model+rules". Otherwise, for example when the model call or its attestation fails, it records"rules-only".
The model only advises. It is shown the invoice text as data, the facts invoice_hash, amount, po_ref and purpose_hash (not your payee_hint), and the one mandate the code picked. The code rules still run on its proposal, and the contract checks the spend again:
- A proposal to hold is recorded as rule
model.refusedwith a hold. A proposal to refuse ismodel.refusedwith a refusal. - A confidence below
confidenceFloorholds the spend withmodel.low_confidence. - An amount that differs from
facts.amountis refused withinvoice.amount_invalid. A payee hint that contradicts the mandate is refused withmandate.payee_mismatch, even when the invoice text talked the model into it. - A proposal that names another mandate is refused, with
invoice.po_mismatch(ormandate.nonewhen that mandate does not exist), so the model cannot move a payment to another mandate. - When NEAR AI Cloud fails, or its attestation fails the checks at this ledger position, the proposal is dropped and the code decides alone, with
model.unavailablein the record.
outcome.modelUsage gives the model id and token counts whenever the model answered. Pass the key only when you have one, so that a missing variable falls back to the code:
model: process.env.NEARAI_API_KEY ? { provider: "near-ai-cloud", apiKey: process.env.NEARAI_API_KEY } : null,Note
The NEAR AI Cloud adapter is built and tested against a fake NEAR AI Cloud, and is Preview. On testnet, the Canary's ledger of 2026-11-10, published in the repository, holds two real attested decisions that pass A1 to A7 offline and, on 2026-11-11, O1 and O2 online (evidence (opens in a new tab), online output (opens in a new tab)). On the testnet demo vault, Payables made five more, which pass A1 to A7 offline; four of them, entries 10 to 13, also passed O1 and O2 online on 2026-11-12. The code refused three of them on 2026-11-11, and on 2026-11-12 it allowed the fourth and the fifth, both by Qwen/Qwen3.8-27B, and the vault paid each invoice, 2.00 test USDC, as spends 4 and 5 (evidence (opens in a new tab), the second (opens in a new tab), online output (opens in a new tab)). The TEE measurement is not pinned, so this does not show which model image ran, and A8 was not run on the Canary's two; on the demo vault's five it passed on 2026-11-12, with the invoices committed in the repository (output (opens in a new tab)). Payment checks explains how the code rules and the contract's checks fit together.
Your own rules#
probarion.rules.add(rule) adds a rule to every decision. Its id starts with custom. and its check(input, mandate) returns a pass, a hold or a refusal with a detail. Custom rules run after the built-in ones and can only tighten the verdict: a rule that throws refuses.
Run the loop#
- Check The input and both keys, before anything is read.
- Read The vault's config, mandates and spends, and your whole ledger.
- Decide Optional model advice, then the code rules.
- Send once A decision entry, the free
check_spend, then onespend. - Record A payment entry, then an anchor on chain.
The cycle in code order#
- Check the input: a malformed call throws
InvalidInput. - Require the keys: no
agentKeyorledgerKeythrowsConfigError. - Read the vault: its config, every mandate and every spend.
- Check the ledger key: yours must be the vault's.
- Read the whole ledger, to find the next position.
- Look in the ledger for an unresolved earlier payment of the same invoice.
- Pick the mandate by
purpose_hash. - Ask the model, when one is set. This is skipped with
model: null, with no matching mandate, or when an earlier payment of the invoice is unresolved. - Decide with the code rules. For a Base or Solana payee, the SDK first reads the vault's balance on NEAR Intents; an unreadable or short balance holds with
intents.balance_low. - Write the decision entry at exactly the position read in step 5.
- Unless the code refused, dry-run the spend with the contract's free
check_spendview. A refusal there ends the cycle asrefused_by_contract. A code hold that the contract would pay at once ends it asheldwith nothing sent. - Send
spendonce, signed by the agent key. It is never sent again. - Write the payment entry. If it cannot be written, the outcome still stands and carries
ledgerComplete: false. - Anchor the ledger tail with the ledger key. A failed anchor is only logged: the next cycle anchors the whole tail.
Once spend is sent, the cycle always returns an outcome, so when process() throws, no spend went out. That is inferred: it follows from the code, and no test shows it yet. A lost answer comes back as unconfirmed, as the SDK's tests show.
One call at a time#
Calls on one instance run one at a time, in call order, so a burst of invoices queues inside the instance. Use one instance per vault in one process. A second instance or a second process is a second writer: see One ledger writer per vault.
Wire it into your agent#
Illustrative, adapted from the SDK README: the imports point at the built files of a clone (with the packages from npm, import @probarion/core and @probarion/sdk by name), the vault is a placeholder, the keys come from the two key files, and model: null lets the code decide alone. Save it in the probarion folder and run it from there. The example invoice is for 2 USDC, so the developer quickstart's mandate (at most 5 USDC per payment, held for an approver above 2) pays it at once. A larger amount needs a mandate whose limits allow it and a vault that holds enough test USDC.
import { readFile } from "node:fs/promises";
import { sha256Hex } from "./packages/core/dist/index.js";
import { Probarion, hashInvoice } from "./packages/sdk/dist/index.js";
// The secret_key of a file that agent keygen wrote.
const secretOf = async (file) => JSON.parse(await readFile(file, "utf8")).secret_key;
// One instance per vault in this process: its process() calls run one at a time.
const probarion = new Probarion({
network: "testnet",
vault: "<your-vault>.vaults.probarion.testnet",
agentKey: await secretOf("../agent-key.json"), // can call only spend
ledgerKey: await secretOf("../ledger-key.json"), // can call only anchor, and signs every ledger entry
model: null, // the code decides alone
});
// The purchase order file the owner signed the mandate with.
const poBytes = new Uint8Array(await readFile("../po.txt"));
// Your agent read the invoice and supplies the facts: the SDK extracts nothing.
export function pay(invoiceText, { ref, amount, poRef }) {
return probarion.process({
text: invoiceText, // untrusted: shown to the model as data and nothing else
ref, // your reference; the vault accepts each ref once
facts: {
invoice_hash: hashInvoice(invoiceText),
amount, // USDC base units: "2000000" is 2 USDC
po_ref: poRef, // copied verbatim from invoiceText, or null
purpose_hash: sha256Hex(poBytes), // the hash the mandate was created with
payee_hint: null, // what the invoice claims; never the destination
},
});
}
const invoiceText = "Invoice INV-2026-001: 2.00 USDC for PO-1001";
const outcome = await pay(invoiceText, { ref: "INV-2026-001", amount: "2000000", poRef: "PO-1001" });
console.log(outcome.status, outcome.code, outcome.spendId, outcome.txHash);A clean cycle writes two entries: the decision, then the payment. On an empty ledger they sit at positions 0 and 1, and on a ledger that already holds entries, such as the developer quickstart's, at the next two positions. This is what the SDK's own test expects from a first payment against its fake chain and fake NEAR AI Cloud:
const text = "Invoice INV-1: 25.00 USDC for PO-1001";
const out = await pb.process(nearInvoice(text, "INV-1"));
expect(out).toMatchObject({ status: "paid", spendId: 1, code: null, ledgerSeq: 1 });Handle the outcome#
outcome.status is one of the seven statuses process() returns. SpendOutcomeStatus also has settled, which process() never returns. Illustrative, one line per branch:
switch (outcome.status) {
case "paid": // the vault paid the mandate's NEAR payee: record spendId and txHash
case "burned": // paid out through NEAR Intents to a Base or Solana payee: record spendId and txHash
break;
case "held": // spendId null: a code rule held it and nothing was sent; spendId set: an approver decides
console.log(outcome.spendId === null ? `code hold: ${outcome.code}` : `held as spend ${outcome.spendId}`);
break;
case "refused_by_code": // a code rule said no (outcome.code is the rule id); nothing was sent
case "refused_by_contract": // the vault said no (outcome.code is the vault code)
break;
case "failed": // definitely not paid; spendId set means the vault took the spend and its ref is used up
break;
case "unconfirmed": // it may have paid: never send it again; find the spend by its ref
break;
}A contract hold waits for one of the vault's approvers, who approves or rejects it before the hold expires: see Holds and approvals. Spend outcomes has the full table, and Errors explains each vault code.
Three rules for every loop
- Never send an
unconfirmedspend again, least of all under a new ref. Wait, or find the spend by its ref. - A
heldoutcome withspendIdnull was never sent, and no call releases it. Change what caused the hold, then run the cycle again. - A
failedoutcome withspendIdset used up its ref. The retry needs a newref.
Find a spend by its ref#
probarion.spends.get(id) takes a spend id, not a ref, and the contract has no lookup by ref. To find a spend by its ref, read the vault's spend list. Illustrative, adapted from the SDK README; save it in the probarion folder too:
import { RpcClient, VaultClient, resolveRpcUrls } from "./packages/sdk/dist/index.js";
// Read-only: no sender, so it cannot sign anything. listAllSpends pages the contract's list_spends.
const vault = new VaultClient("<your-vault>.vaults.probarion.testnet", new RpcClient({ urls: resolveRpcUrls("testnet") }));
const spend = (await vault.listAllSpends()).find((s) => s.ref === "INV-2026-001") ?? null;
console.log(spend === null ? "no spend with this ref" : `spend ${spend.id}: ${spend.status}`);Refs and retries#
- A ref is used once per vault, forever. It is used up once the contract accepts a spend, held or paid, and it stays used even if the payout fails, the spend is rejected or the hold expires. A refused spend reverts every change, so its ref stays free, and a code refusal sends nothing at all.
- The SDK refuses a used ref by code. Before anything is sent, the rule
invoice.duplicaterefuses a ref the vault has already used, and an invoice with a live spend (held, pending, paid or burned). The contract checks both again, as DuplicateInvoice and DuplicateRef. - A released invoice can be paid again. When the vault releases a spend (failed, rejected or expired), the same invoice can go through under a new ref.
- An unconfirmed invoice waits. After
unconfirmed, the same invoice is refused by code withledger.unresolveduntil the chain shows a spend with that decision's hash, a later payment entry records a known outcome, or 48 hours pass (UNKNOWN_SETTLE_MS). The refusal's detail names the time after which the invoice can be retried. The guard lives in your ledger: a writer with another ledger file does not see it. - Dedupe by
spendId. On one instance, a second identical call is refused by code withinvoice.duplicate. Two writers that make identical decisions (for example with the samerequestId) can both report the samespendIdas paid, for one payment. Never count two successes as two payments.
The SDK's test for a failed payout shows the sequence: the first spend fails and is released, the same ref is refused by code, and the same invoice pays under a new ref.
server.chain.hooks.payoutFailure = "ft_transfer_failed";
const { pb, ledger } = setup(server);
const first = await pb.process(INVOICE());
expect(first).toMatchObject({ status: "failed", spendId: 1, code: "failed", ledgerSeq: 1 });
expect(typeof first.txHash).toBe("string");
expect(server.chain.spends[0]?.status).toBe("failed");
expect((await ledger.all())[1]?.body.contract).toEqual({
code: null,
tx_hash: first.txHash,
spend_id: 1,
status: "failed",
});
server.chain.hooks.payoutFailure = null;
// The ref is consumed forever, even by a failed spend.
const sameRef = await pb.process(INVOICE());
expect(sameRef).toMatchObject({ status: "refused_by_code", code: "invoice.duplicate" });
// A released spend frees its invoice: the same invoice under a new ref is a new decision.
const retry = await pb.process({ ...INVOICE(), ref: "INV-1-retry" });
expect(retry).toMatchObject({ status: "paid", spendId: 2 });One ledger writer per vault#
The ledger is a file by default: .probarion/ledger.jsonl in the working folder, or the path in PROBARION_LEDGER_PATH. It holds one JSON entry per line. Each append takes a lock file beside it (<path>.lock) and is synced to disk. The lock is made with a hard link, so the folder must be on a local disk whose file system supports hard links (NTFS, ext4, APFS), not FAT, exFAT or some network shares.
- Two writers on one file. The cycle whose decision append finds that the ledger moved throws
LedgerConflictError(Errors) withledger moved since this decision was attested; retry (no entry was written and no spend was sent): .... Nothing is written or sent, and the SDK never retries it by itself. - Two ledger files for one vault. They fork its history. The contract accepts each writer's anchor as long as its count is not below the previous anchor's, and the anchor check of
verify(check c) then fails. That is inferred: it follows from the code, and no test shows it yet. A cycle that finds the vault anchored past its own ledger logs a warning and does not anchor. - Your own store. A
LedgerStoreyou pass in, for example over a database, must honour the expected position on append. If it does not, the SDK stops with aLedgerErrorbefore it sends the spend.
The ledger and anchors explains what each entry records.
Not on a vault that runs hosted Payables
Do not also run the console's hosted Payables on the same vault. Payables keeps its own ledger for its vault, in the console's database, signed with a ledger key that Probarion's server holds. A vault has one ledger key, and process() refuses to run when its ledgerKey is not the vault's, so the two cannot both write to one vault. This is inferred: it follows from the code, and no test shows it yet. Give your SDK agent a vault of its own.
Check your work#
Run the verifier from the same folder as your loop, so it reads the same default ledger file. Pass --ledger <path> when your ledger lives elsewhere.
node packages/cli/dist/bin.js --network testnet verify --vault <your-vault>.vaults.probarion.testnetThe report starts with Probarion verify: <vault> (testnet), lists each check, and ends with a RESULT: line that says PASS, FAIL or UNPROVEN. The exit code says the same:
| Exit code | Meaning |
|---|---|
| 0 | Every check passes. |
| 1 | A check fails, or verify could not run. |
| 2 | No check fails, but at least one is unproven. |
| 64 | A usage error: a missing, malformed or conflicting flag. |
Verify a vault yourself walks through a run.
Common mistakes#
- Sending an
unconfirmedspend again under a new ref. It may have paid. Find the spend by its ref, or wait forledger.unresolvedto clear. - Retrying a
failedspend with its old ref. WhenspendIdis set, the vault used the ref, and the SDK refuses it withinvoice.duplicate. Give the retry a new ref. - A second instance, a second process or a second ledger file on one vault. The loser of a race throws
LedgerConflictError, and a second file forks the history. apiKey: process.env.NEARAI_API_KEY ?? "". An empty key throwsConfigErrorwhen the variable is unset. Passmodel: null, or setmodelonly when the key exists.- An amount in decimals, such as
"25.00". It is refused withinvoice.amount_invalid. Write"25000000". - A
po_refthat was re-cased, padded or normalised (for example a no-break space turned into a space). It throwsInvalidInput. Copy it from the text byte for byte, or passnull. - The hash of another file as
purpose_hash. No mandate matches, and the cycle is refused withmandate.none. Hash the file the mandate was signed with. - NotAgent from the vault. The mandate names your agent key, but the vault does not hold it yet: the owner's
agent addhas not landed. refused_by_codewithmandate.agent_mismatch.agentKeyis not the key the mandate names, for example because the agent key was made again after you sent its public key. Pass the secret of the public key you sent the owner.
Not available yet#
- No HTTP API for agents: the hosted invoice API needs a signed-in console session of the vault's owner or approver. An agent pays from its own process, through the SDK. The CLI sends no spend:
checkonly dry-runs one, and its owner and approver commands only print transactions.
What works today lists the status of every part.
Related pages#
- Developer quickstart: one payment end to end.
- Spend outcomes: every status and code in detail.
- Errors: the vault, SDK and HTTP error codes.
- Mandates and Holds and approvals: what a mandate allows, and what happens to a held spend.
- The ledger and anchors: what each entry records.
- Verify a vault yourself: run the verifier and read its report.
- The SDK README (opens in a new tab) lists the rest of the
Probarionclass.
Built from 4ab41ca · reviewed
packages/sdk/src/probarion.ts(opens in a new tab)packages/sdk/src/input.ts(opens in a new tab)packages/sdk/src/spend-outcome.ts(opens in a new tab)packages/sdk/src/context.ts(opens in a new tab)packages/sdk/src/ledger-store.ts(opens in a new tab)packages/sdk/src/rpc.ts(opens in a new tab)packages/sdk/src/nearai.ts(opens in a new tab)packages/sdk/src/tx.ts(opens in a new tab)packages/sdk/src/vault-client.ts(opens in a new tab)packages/sdk/src/mutex.ts(opens in a new tab)packages/core/src/policy.ts(opens in a new tab)packages/core/src/proposal-request.ts(opens in a new tab)packages/core/src/hash.ts(opens in a new tab)packages/core/src/types.ts(opens in a new tab)packages/sdk/README.md(opens in a new tab)packages/sdk/test/probarion.integration.test.ts(opens in a new tab)packages/sdk/test/probarion.outcomes.test.ts(opens in a new tab)packages/sdk/test/probarion.input.test.ts(opens in a new tab)packages/sdk/test/support/probarion-harness.ts(opens in a new tab)packages/cli/README.md(opens in a new tab)packages/cli/src/commands/agent.ts(opens in a new tab)packages/cli/src/commands/check.ts(opens in a new tab)packages/cli/src/commands/ledger.ts(opens in a new tab)packages/cli/src/commands/mandate.ts(opens in a new tab)packages/cli/src/commands/owner-calls.ts(opens in a new tab)packages/cli/src/commands/verify.ts(opens in a new tab)packages/cli/src/owner-tx.ts(opens in a new tab)packages/cli/src/report.ts(opens in a new tab)contracts/vault/src/keys.rs(opens in a new tab)contracts/vault/src/checks.rs(opens in a new tab)contracts/vault/src/lib.rs(opens in a new tab)contracts/vault/tests/invariants.rs(opens in a new tab)apps/web/src/lib/topup/copy.ts(opens in a new tab)apps/web/src/lib/topup/faucet.ts(opens in a new tab)apps/web/src/lib/ratelimit.ts(opens in a new tab)apps/web/src/lib/auth/guard.ts(opens in a new tab)apps/web/src/app/api/console/[vault]/invoices/route.ts(opens in a new tab)apps/web/src/lib/payables/pipeline.ts(opens in a new tab)apps/web/src/lib/ledger/store.ts(opens in a new tab)SECURITY.md(opens in a new tab)README.md(opens in a new tab)contracts/vault/src/anchors.rs(opens in a new tab)packages/sdk/package.json(opens in a new tab)packages/core/package.json(opens in a new tab)apps/web/src/lib/console/checklist.ts(opens in a new tab)apps/web/src/components/console/FirstPaymentChecklist.tsx(opens in a new tab)apps/web/src/lib/console/key-readiness.ts(opens in a new tab)apps/web/src/app/console/[vault]/page.tsx(opens in a new tab)apps/web/src/app/console/[vault]/agent/page.tsx(opens in a new tab)apps/web/src/components/console/ConnectAgentPanel.tsx(opens in a new tab)apps/web/src/lib/agent/connect.ts(opens in a new tab)packages/mcp/src/server.ts(opens in a new tab)docs/evidence/canary-vault/README.md(opens in a new tab)docs/evidence/canary-vault/verify-online-2026-11-11.txt(opens in a new tab)contracts/deployments/mainnet.json(opens in a new tab)docs/evidence/trial-vault/README.md(opens in a new tab)docs/evidence/demo-vault/README.md(opens in a new tab)docs/evidence/demo-vault/verify-online-2026-11-12.txt(opens in a new tab)