# Security model

> Who holds which key, what the vault enforces, the loss bounds if a key leaks, how to stop a vault, the known gaps, and how to report a flaw.

Probarion splits control between the owner's wallet, the approvers, the vault's own function-call keys and code that runs off chain. This page shows what each can do, what the contract enforces whatever that code does, and the most a leaked key can cost.

Status: Live · Network: Testnet · Reviewed: 2026-11-12 · Built from: 4ab41ca1436589c22073fa842ee46f970415e3d5

Canonical page: https://probarion.xyz/docs/trust/security-model

> **Warning**
>
> **Unaudited beta.** No third party has audited the contracts or the packages.

A spend is checked by code off chain and again by the contract on chain, and every decision goes into a signed, hash-chained ledger. The contract is the layer that holds whatever the off-chain code does: a leaked agent key can spend only what the mandates naming it still allow, and only to the payees they name. Probarion's servers hold no owner key, and the CLI and the console sign no owner or approver transaction. The owner's full-access key is the one key with no bound inside the vault. Today the vault runs on NEAR testnet, with test tokens, and one small vault, `v0.probarion.near`, holds a little real USDC on mainnet. Its one spend's decision is pre-ledger: a decision file in the repository, with no signed ledger and no attestation. No Base payout has run on mainnet.

1. **Code** `decide()` in `@probarion/core` runs the rules on every decision. A model, when one is used, only proposes.
2. **Contract** `spend` runs 14 checks in order. The first one that fails refuses the spend, and the vault stays as it was.
3. **Ledger** The SDK signs each decision and its outcome into a hash-chained ledger, and anchors the ledger on the vault.

## Shared responsibility

This table follows the key table of [SECURITY.md](https://github.com/probarion/probarion/blob/main/SECURITY.md#threat-model).

| Who | Can | Cannot | What stops it |
| - | - | - | - |
| **The agent key**, a function-call key the vault adds to itself, method `spend` only | Call `spend` under a mandate that names this key | Choose the payee. Go past the mandate's `max_per_payment`, `max_total` (spent plus reserved), `max_uses` or `expires_at_ns`, or the vault's daily backstop. Pay one `invoice_hash` twice or reuse a `ref`. Call any other method, or attach a deposit | `spend` has no address field, so the destination comes from the mandate in storage. The 14 checks refuse the rest. The NEAR protocol limits a function-call key to its method list and to calls without a deposit |
| **The ledger key**, a function-call key on the vault, method `anchor` only | Anchor the next ledger range, with `seq` exactly one above the last | Spend, or change a mandate or a key | The NEAR protocol limits it to `anchor`, and `anchor` checks the key and the `seq` |
| **Whoever holds the agent key**: the agent's operator, or anyone who took it from a server | Spend inside the open mandates that name it, with invoice hashes of its choosing | Pay anyone a mandate does not name, or exceed a mandate's limits or the daily backstop. Hide a rewrite of a ledger range that was anchored | The contract's checks, and the anchors: `verify` fails on the first anchor whose range no longer matches, and names the first entry of that range |
| **The owner**, a full-access key on the owner's account | Create and revoke mandates, add and remove keys, set the ledger key, freeze, unfreeze, and withdraw the USDC and the Intents balance to the owner's own account | Change a mandate's payee, change the owner or the approvers, upgrade the vault, or withdraw to another account. Approve or reject a held spend, unless the owner is also an approver | No method changes a payee, the owner or the approvers, upgrades the vault or withdraws to another account. A new payee needs a new mandate. `approve` and `reject` refuse anyone not on the approver list with [`NotApprover`](https://probarion.xyz/docs/build/errors.md#NotApprover) |
| **A function-call key on the owner's or an approver's account**, such as a dapp login key | On the owner's account, call `freeze`, if the key may call this vault: its receiver is the vault, and its method list is empty or names `freeze` | Call any other owner or approver method: `add_agent`, `remove_agent`, `set_ledger_key`, `create_mandate`, `revoke_mandate`, `approve`, `reject`, `unfreeze`, `withdraw` or `withdraw_intents` | Each needs a deposit, exactly 1 yoctoNEAR or the storage deposit of `create_mandate`, and only a full-access key can attach one |
| **An approver**, one of the accounts named when the vault was created | Approve or reject a held spend, alone | Approve while the vault is frozen, after the hold expired, or for a revoked mandate. Change a mandate, a key or a setting | `approve` checks the freeze, the hold time and the mandate. No method lets an approver change anything else |
| **The factory owner** | Publish a new vault version | Change, upgrade or take funds from a vault the factory created | Such a vault starts with no access key and has no upgrade method |
| **The model**, on NEAR AI Cloud in a private TEE | Propose `pay`, `hold` or `refuse` for one invoice | Decide alone, or widen a mandate with text injected into an invoice | The code rules run on every decision, and the contract checks every spend |
| **An extension token**, held by a browser connected on the site's signed-in extension page, Preview. The server keeps a sha256 of the token and its last four characters, not the token itself | For 30 days unless revoked, on its own network: read its account's waiting invoices, held spends and active mandates, and queue an invoice (`POST /api/ext/invoices`, at most 20 per account and network in 10 minutes) for a vault where the account is an owner or an approver, not a Canary vault, under a usable mandate. The server stores the invoice in `awaiting_owner` only when core's `payeeHintContradiction` and the contract's `check_spend` view let it through, and otherwise stores nothing | Pay, approve, confirm, or change a mandate or a key. A queued invoice runs no cycle and waits for the owner's wallet-signed Confirm | The extension's server code imports no function that pays or signs, and none that opens a sealed key from outside `lib/crypto`. `secrets.ts` opens a sealed value only inside its own opening functions, which that code does not import, and the queue's route reads the master key only to seal the invoice with `sealInvoice` and to key its duplicate guard, as the console upload does. A structure test checks those imports. Review of each change to that code is the other control. Confirm needs the owner's wallet signature |

[Vaults, roles and keys](https://probarion.xyz/docs/concepts/vaults-roles-and-keys.md) explains each role and key in full.

### Who answers for what

- **The owner** sets every limit, from their own wallet, and keeps the owner account's full-access key safe. Only the owner can withdraw, unfreeze or change a key.
- **The approvers** decide each held spend, each from their own wallet.
- **The agent's operator** runs the agent and keeps its agent and ledger keys, unless the console made them, and runs one ledger writer per vault.
- **Probarion** runs the console and the testnet factory. Its CLI and console hand each owner and approver transaction to that person's wallet, and sign none of them.

### What Probarion holds

**No owner or approver key.** The owner's and the approvers' full-access keys are never on Probarion's servers, so Probarion cannot change your vault, withdraw from it or approve its spends.

**Sealed agent and ledger keys, for keys the console made.** When the console's Keys tab makes a vault's agent and ledger keys, Probarion's server generates them and stores each secret sealed with libsodium's secretbox, under a master key held outside the database. Payables, the console's reference agent, signs with them, and so does the Canary, Probarion's own public-challenge vault, which is Live on testnet. The owner adds each key to the vault with its public key only. The Keys tab is Preview, and Payables is Live on testnet. A key you make with `agent keygen` stays with you, and Probarion never sees it.

An invoice that Payables cannot pay on its own, because no attested model confirmed its amount, waits in the Payables tab for the owner's Confirm. Confirm is a NEP-413 signature from the owner's wallet, not a transaction, and the server checks that a full-access key of the vault's owner signed it before it pays.

**A mint-only faucet key, testnet only.** The server signs the Top-up tab's test-USDC mint as `probarion.testnet`, with a function-call key that may call only `mint` on testnet USDC, and it refuses to sign when the key on chain allows anything more. That key cannot call a vault. The faucet is Preview.

## What the vault enforces

These hold on chain, whatever the SDK, the console or the agent does.

- **The 14 spend checks.** `spend` runs [the 14 checks, in order](https://probarion.xyz/docs/concepts/payment-checks.md#the-14-checks-in-order): the agent key, the request's shape, the freeze, the mandate, its expiry and agent, the amount, the uses, the total, the invoice, the ref and the daily backstop. The first one that fails refuses the spend and changes nothing. [Mandates](https://probarion.xyz/docs/concepts/mandates.md) covers each limit.
- **No payee in a spend.** The request carries a mandate id, an amount, an invoice hash, a ref and a decision hash. The destination comes only from the mandate the owner signed. [Why spend has no payee](https://probarion.xyz/docs/concepts/why-spend-has-no-payee.md) explains why.
- **The one-yocto rule.** Most owner methods, and both approver methods, need exactly 1 yoctoNEAR attached. Only a full-access key can attach a deposit, through a wallet prompt, so a dapp login key cannot make these calls, and a script in a web page cannot make them without that prompt. `create_mandate` takes a storage deposit and sends back the excess. `freeze` is the exception: it takes no deposit, so the owner can stop the vault fast.
- **Two kinds of key.** The vault adds keys to itself only on the owner's instruction: function-call keys for `spend` (`add_agent`) and one for `anchor` (`set_ledger_key`). A vault the factory creates starts with no access key. Sandbox tests show that the NEAR protocol stops the agent key from calling any other method or attaching a deposit, and stops the ledger key from calling `spend`. [The agent key cannot approve itself](https://probarion.xyz/docs/trust/self-approval.md) replays the attack of the I-HITL paper with the agent key, one refusal at a time.
- **Fixed settings.** No method changes the owner, the approvers, the daily backstop or the hold time, and no method edits a mandate. There is no upgrade method: a fix ships as a new vault version, and owners move to a new vault.

> **Key idea**
>
> The model proposes. The code rules and the contract decide.

## Loss bounds

Each bound below is what the code allows, not a promise.

### If the agent key leaks

Whoever holds it can spend only under the open mandates that name that key:

- per spend, at most the mandate's `max_per_payment`;
- in all, at most what is left of `max_total` (spent plus reserved), within the uses left and before `expires_at_ns`;
- across the whole vault, at most the `daily_backstop` in each UTC day, one counter for every mandate and key;
- only to the payee each mandate names;
- a spend above the mandate's `approval_above` waits for an approver.

Spends at or under `approval_above` pay at once, and the key can send many of them. So the bound is the mandates' remaining totals, capped by the daily backstop for each day until they expire, not `approval_above`.

The holder can skip the SDK and its off-chain rules by calling `spend` directly: the contract checks only that `decision_hash` is 64 hex characters, and never matches it with a decision. [Check d](https://probarion.xyz/docs/trust/verifier-checks.md#check-d) of the verifier then fails, and names the first spend that has no decision of its own in the ledger or that reuses a decision already bound to another spend. That catches a thief who holds only the agent key. One who also holds the ledger key and can write the ledger, as on a breached server, or on a breached Probarion for keys the console made, can sign a decision for each spend, and check d then passes, because the verifier does not run the policy again. This follows from the verifier's code; no test shows it yet. Every call the key signs, refused or not, burns gas from the vault's NEAR, up to the key's allowance: 1 NEAR by default in the console and the CLI.

To stop it, freeze the vault, then remove the key and revoke the mandates that name it. Spends already held still wait for an approver after the key is removed, and an approver can reject them, because `approve` and `reject` do not look at the agent key. This follows from the contract's code; no test shows it yet. [Remove an agent key](https://probarion.xyz/docs/concepts/vaults-roles-and-keys.md#remove-an-agent-key) has the details.

> **Owner signs**
>
> The owner signs `freeze`, `remove_agent` and `revoke_mandate` from their own wallet. `freeze` takes no deposit. The other two need exactly 1 yoctoNEAR.
>
> _The owner signs this transaction. Probarion never signs it._

### If the ledger key leaks

It cannot move money or change a mandate. The NEAR protocol stops it from calling `spend`, and its only method is `anchor`. Whoever holds it can sign new ledger entries and anchor the next range, so they can write a false record after the last anchor. Its gas allowance, 0.25 NEAR by default, caps what it burns.

Rotate it: the owner sets a new ledger key with `set_ledger_key`, `ledger set-key` in the CLI. Each anchor stores the key that signed it, so the ranges anchored before the rotation stay verifiable, and the vault deletes the old key's access key, so nothing the old key signs reaches the vault any more. [Rotate the ledger key](https://probarion.xyz/docs/concepts/vaults-roles-and-keys.md#rotate-the-ledger-key) gives the order: anchor first, then rotate.

> **Owner signs**
>
> The owner signs `set_ledger_key` from their own wallet, with exactly 1 yoctoNEAR attached. The transaction carries the new public key only.
>
> _The owner signs this transaction. Probarion never signs it._

### If the owner key leaks

A full-access key on the owner's account acts as the owner, because the vault checks only which account calls. Its holder can do everything the owner can: create a mandate to any payee with an agent key of their own, unfreeze, and withdraw. `withdraw` sends USDC only to the owner's account, but the holder controls that account too. `withdraw` also ignores reservations, so it can take out USDC that held spends need: an approved payout then fails and is released. This follows from the contract's code; no test shows it yet. Freezing does not stop the holder, who can unfreeze. There is no bound inside the vault, so keep the owner's full-access key safe.

### If the owner key is lost

If every full-access key on the owner's account is lost, no other account can withdraw, unfreeze, revoke a mandate or change a key. The vault has no recovery method and no method that changes its owner, so no one can withdraw its USDC to an account: only spends to the payees of open mandates still move it. Agent keys can still spend under those mandates until they expire or run out, and approvers can still decide held spends. No method withdraws the vault's NEAR either: what leaves it is the gas its keys burn, within their allowances, and the 1 yoctoNEAR a payout attaches to its token transfer. This follows from the vault's method list; no test shows it yet.

### If an approver key leaks

Its holder can approve every held spend that is still alive. One approver is enough: there is no quorum. Each payout still goes to its mandate's payee, for the amount reserved when the spend was held. `approve` does not check the mandate's expiry, uses, total or the daily backstop again, because the reservation already counted them: this follows from the contract's code, and no test shows it yet. The holder can also reject held spends, which releases them.

The approver list holds accounts and cannot change, so the approver's account stays an approver of this vault. The approver deletes the leaked key from their own account, if they still hold another full-access key on it. Until then, the owner stops approvals by freezing the vault or revoking the mandates of the held spends. If the approver has lost control of the account, the owner moves to a new vault to drop it. [Holds and approvals](https://probarion.xyz/docs/concepts/holds-and-approvals.md) covers what `approve` checks.

> **Approver signs**
>
> An approver signs `approve` or `reject` from their own wallet, with exactly 1 yoctoNEAR attached. A login key cannot attach it, so only a full-access key on the approver's account can decide a held spend.
>
> _An approver signs this transaction. Probarion never signs it._

### If Probarion is breached

A vault whose agent and ledger keys the console made has them on Probarion's servers, so the agent key and ledger key bounds above apply to it. A vault whose keys you made with `agent keygen` has no key there. The threat model's blast radius, checked against the code:

| An attacker in full control of Probarion's servers can | But cannot |
| - | - |
| Call `spend` with the stolen agent keys | Pay anyone not named in a mandate |
| Submit fake invoice hashes against open mandates, or pay a waiting invoice without the owner's wallet-signed Confirm | Exceed a mandate's per-payment cap, total cap, uses or expiry, or the vault's daily backstop. A spend above `approval_above` still waits for an approver |
| Rewrite off-chain history | Hide a rewrite of a range already anchored: check c fails on the first anchor whose root no longer matches, and names the first entry of its range. An anchor pins a whole range, so check c cannot say which entry inside it changed |
| Stop the service | Lock the owner out of the funds: the owner can freeze, withdraw and remove keys with their own keys, for example with near-cli-rs, without Probarion |
| Use a hijacked owner or approver session, or a function-call key on their account, such as a dapp login key | Create or revoke a mandate, approve or reject, withdraw, add or rotate keys, or unfreeze: each needs a deposit, exactly 1 yoctoNEAR or the storage deposit of `create_mandate`, which only a full-access key can attach, through a wallet prompt |

Whoever holds the master key and a copy of the database holds every sealed agent and ledger key, and can forge console sessions. That is still bounded by the table above. If the master key, `KEY_ENCRYPTION_SECRET`, is lost, the sealed keys are lost with it, not the funds: remove those keys from each vault and add new ones.

## Stop a vault

The fastest stop is in the console. Signed in as the vault's owner, open the "Overview" tab: the "Vault" card has a "Freeze vault" button, and your wallet signs the call. Its confirmation reads "Freeze stops every spend and every approval until you unfreeze. Continue?" When the vault's config cannot be read, the tab still offers the button, on an "Emergency stop" card.

The CLI is the stop that does not depend on Probarion's console. It prints the transaction, with no deposit, for your own wallet or near-cli-rs, and signs nothing. [Install](https://probarion.xyz/docs/build/install.md) builds it from a clone.

> **Owner signs**
>
> The owner signs `freeze` with no deposit, from the owner's full-access key or from a function-call key on the owner's account whose receiver is the vault and whose method list is empty or names `freeze`, such as a dapp login key scoped to the vault. A key scoped to another contract cannot call the vault at all. `unfreeze` needs exactly 1 yoctoNEAR and fails without it with [`RequiresOneYocto`](https://probarion.xyz/docs/build/errors.md#RequiresOneYocto), so a login key that froze the vault cannot unfreeze it.
>
> _The owner signs this transaction. Probarion never signs it._

```bash
node packages/cli/dist/bin.js --network testnet vault freeze --vault <vault>
```

Illustrative, shortened: the two lines after the command, with the args file's sha256, and the last line, about a Ledger wallet, are left out.

```text
OWNER SIGNS: probarion never signs owner or approver transactions. Review the arguments, then sign with your own wallet or near-cli-rs:

near contract call-function as-transaction <vault> freeze file-args ./freeze.<12 hex>.args.json prepaid-gas '30.0 Tgas' attached-deposit '0 NEAR' sign-as <owner-account-id> network-config testnet sign-with-keychain send

freeze arguments (./freeze.<12 hex>.args.json):
{}

Every new spend and every approve is refused until unfreeze. reject and expire_held still work.
```

While the vault is frozen:

- `spend` refuses with [`Frozen`](https://probarion.xyz/docs/build/errors.md#Frozen), and the `check_spend` dry run answers `Frozen`.
- `approve` refuses with `Frozen`.
- Payouts already sent still settle or are released by their callbacks.
- `reject` and `expire_held` still work.
- The owner can still withdraw and change keys: no withdraw or key method checks the freeze. This follows from the contract's code; no test shows it yet.

Probarion can freeze only the vaults it owns: `freeze` checks the vault's owner, so the owner of any other vault freezes it from their own wallet. [Freeze is the exception](https://probarion.xyz/docs/concepts/vaults-roles-and-keys.md#freeze-is-the-exception) shows the code.

## Known gaps

- **No Content-Security-Policy yet.** An XSS in the console has no second wall in the browser. It still cannot attach a deposit, but it could show the owner a false page, start a wallet prompt, or mint a 30-day extension token or a pairing code. Source: [THREAT_MODEL.md, known gaps](https://github.com/probarion/probarion/blob/main/docs/THREAT_MODEL.md#known-gaps).
- **Extension tokens outlive the session that made them.** A token lasts 30 days unless it is revoked. Signing out, the session's 12-hour expiry and a new `KEY_ENCRYPTION_SECRET` do not end it, because the database keeps it as a plain sha256, not sealed under the master key. It reads the waiting list and can add to it: up to 20 invoices in 10 minutes, each under a usable mandate, to the payee that mandate already names. It cannot pay: each invoice waits for the owner's wallet-signed Confirm, and the owner can reject it. A queued invoice can block a later upload of the same invoice until the owner rejects it. The account a token belongs to revokes it on the extension page, and the vault's owner cannot revoke an approver's token. A pairing code the session made outlives it too, for up to 10 minutes. Source: [THREAT_MODEL.md, known gaps](https://github.com/probarion/probarion/blob/main/docs/THREAT_MODEL.md#known-gaps).
- **`wee_alloc` in the shipped contracts.** near-sdk 5.29.1 turns on its `wee_alloc` allocator by default, and Dependabot flags it as unmaintained (GHSA-rc23-xxgq-x27g). Turning it off changes the code hash, so the fix ships only as a new vault version. Source: [THREAT_MODEL.md, known gaps](https://github.com/probarion/probarion/blob/main/docs/THREAT_MODEL.md#known-gaps) and [`contracts/Cargo.lock`](https://github.com/probarion/probarion/blob/main/contracts/Cargo.lock).
- **`cargo audit` not run.** CI has no dependency-audit job. Dependabot alerts are triaged, and the threat model's review log records each triage. Source: [THREAT_MODEL.md, known gaps](https://github.com/probarion/probarion/blob/main/docs/THREAT_MODEL.md#known-gaps).
- **Secret scanning looks for provider patterns only.** Private vulnerability reporting, secret scanning, push protection and Dependabot security updates are on for the repository; secret scanning's non-provider patterns and validity checks are off. CI's gitleaks job scans the history on pushes and pull requests to `main` and `dev`. Source: [THREAT_MODEL.md, known gaps](https://github.com/probarion/probarion/blob/main/docs/THREAT_MODEL.md#known-gaps).
- **No security email address.** A report goes by a direct message on X or the repository's private GitHub form. There is no email address until one passes a delivery test. Source: [THREAT_MODEL.md, known gaps](https://github.com/probarion/probarion/blob/main/docs/THREAT_MODEL.md#known-gaps).
- **The TEE measurement is not pinned.** The attestation checks show that a model's signature, response and recorded proposal agree, not that it ran a given model image. Source: [SECURITY.md, what is not proven yet](https://github.com/probarion/probarion/blob/main/SECURITY.md#what-is-not-proven-yet).
- **Real attested decisions do not show which image ran.** The NEAR AI Cloud adapter is tested against a fake NEAR AI Cloud. 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 ([online output](https://github.com/probarion/probarion/blob/main/docs/evidence/canary-vault/verify-online-2026-11-11.txt)). The testnet demo vault's ledger holds five more, made by Payables, which pass A1 to A7 offline; four of them, entries 10 to 13, also passed O1 and O2 online on 2026-11-12 ([online output](https://github.com/probarion/probarion/blob/main/docs/evidence/demo-vault/verify-online-2026-11-12.txt)). The code allowed the last two of them, and the vault paid both invoices, 2.00 test USDC each. 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](https://github.com/probarion/probarion/blob/main/docs/evidence/demo-vault/verify-a8-2026-11-12.txt)). Source: [SECURITY.md, what is not proven yet](https://github.com/probarion/probarion/blob/main/SECURITY.md#what-is-not-proven-yet), [the Canary's evidence](https://github.com/probarion/probarion/blob/main/docs/evidence/canary-vault/README.md#the-signed-ledger-published-2026-11-11), [the demo vault's](https://github.com/probarion/probarion/blob/main/docs/evidence/demo-vault/README.md#2026-11-12-payables-pays-on-its-own-with-an-attested-model) and [its second payment](https://github.com/probarion/probarion/blob/main/docs/evidence/demo-vault/README.md#2026-11-12-filmed-a-second-invoice-paid-on-its-own-freeze-and-unfreeze).
- **A8 needs the invoice text.** The ledger stores no invoice text, so only someone who holds the invoices can show that the model saw the standard request for each one. The CLI built from the repository runs A8 with `--invoice <file>` or `--invoices <dir>`, live or with `--offline`; release 0.1.1 on npm does not have these flags. Source: [SECURITY.md, what is not proven yet](https://github.com/probarion/probarion/blob/main/SECURITY.md#what-is-not-proven-yet).
- **Offline, a snapshot is checked against itself.** `verify --offline` takes the chain data from the same file as the ledger, so a forged snapshot with re-signed entries passes. Take the snapshot yourself, or run live with `--vault`. Source: [SECURITY.md, what is not proven yet](https://github.com/probarion/probarion/blob/main/SECURITY.md#what-is-not-proven-yet).
- **Entries after the last anchor** are checked against the ledger key only, since no anchor pins them yet. [The gap after the last anchor](https://probarion.xyz/docs/concepts/ledger-and-anchors.md#the-gap-after-the-last-anchor) explains it. Source: [SECURITY.md, what is not proven yet](https://github.com/probarion/probarion/blob/main/SECURITY.md#what-is-not-proven-yet).
- **`v0.probarion.testnet` runs an older build,** from before the `set_ledger_key` fix of 2026-11-03. Vaults the testnet factory creates run the fixed build. Source: [README, status](https://github.com/probarion/probarion/blob/main/README.md#status).
- **The factory account keeps a full-access key.** `vaults.probarion.testnet` was created with one, the runbook has no step that deletes it (its optional lock step is for the mainnet factory), and no lock is recorded. Whoever holds that key could deploy new code to the factory, and with it change the version registry that checks [f](https://probarion.xyz/docs/trust/verifier-checks.md#check-f) and [i](https://probarion.xyz/docs/trust/verifier-checks.md#check-i) read. A version added that way would pass check f for the vaults the factory creates from it, and check i too. Vaults the factory already created keep their code. Source: [`docs/runbooks/factory.md`](https://github.com/probarion/probarion/blob/main/docs/runbooks/factory.md) and [`contracts/deployments/testnet.json`](https://github.com/probarion/probarion/blob/main/contracts/deployments/testnet.json).

## Report a vulnerability

Do not put the details of a vulnerability in a public issue, pull request or post.

- **On X:** send a direct message to [@probarionhq](https://x.com/probarionhq) that says you have a security report, with no details. The reply gives you a private channel for them.
- **On GitHub:** use the repository's private form, [**Security → Report a vulnerability**](https://github.com/probarion/probarion/security/advisories/new). It opens a private security advisory that only you and the maintainers can see.

Say what is affected, how to reproduce it, and what an attacker could do with it. The aim is to acknowledge a report within 72 hours. There is no bug bounty. Test only against vaults you deployed yourself, on testnet or in the near-workspaces sandbox, and do not try to move funds that are not yours. [SECURITY.md](https://github.com/probarion/probarion/blob/main/SECURITY.md#reporting-a-vulnerability) is the full policy.

## Where to go next

- [THREAT_MODEL.md](https://github.com/probarion/probarion/blob/main/docs/THREAT_MODEL.md) on GitHub: the eleven attacks, the failure modes and the trust assumptions.
- [Vaults, roles and keys](https://probarion.xyz/docs/concepts/vaults-roles-and-keys.md): each key, where it lives, and how to replace it.
- [Payment checks](https://probarion.xyz/docs/concepts/payment-checks.md): the 14 checks every spend meets.
- [Mandates](https://probarion.xyz/docs/concepts/mandates.md): every limit a spend counts against.
- [Holds and approvals](https://probarion.xyz/docs/concepts/holds-and-approvals.md): what an approver decides, and when a hold expires.
- [The agent key cannot approve itself](https://probarion.xyz/docs/trust/self-approval.md): the I-HITL paper's attack, replayed with the agent key, and what an owner or approver key on the agent's machine still allows.
- [Why spend has no payee](https://probarion.xyz/docs/concepts/why-spend-has-no-payee.md): how the destination stays out of the agent's hands.
- [Ledger and anchors](https://probarion.xyz/docs/concepts/ledger-and-anchors.md): what the ledger key signs and anchors.
- [Verify a vault](https://probarion.xyz/docs/trust/verify-a-vault.md): run the verifier on a vault yourself.
- [Verifier checks](https://probarion.xyz/docs/trust/verifier-checks.md): what each of checks a to i proves.
- [What works today](https://probarion.xyz/docs/start/what-works-today.md): what is Live, Preview and Planned.
- [Testnet and mainnet](https://probarion.xyz/docs/start/testnet-and-mainnet.md): what differs between the two networks.

## What this does not do

- **It does not run the bridge or the NEAR Intents contract.** The POA (Proof of Authority) bridge releases funds on Base and Solana for valid burns, and Probarion trusts it to. `intents.near`, the NEAR Intents contract, has DAO roles that can force-withdraw or lock a balance there, which is why a vault keeps little inside intents. Probarion's `verify` command cannot prove what either one does. On testnet `intents-stub` stands in for `intents.near`, and no burn leaves NEAR.
- **It does not check that an invoice is authentic.** The ledger stores an invoice's hash, not its text.
- **It does not let hosted Payables and your own SDK agent share a vault.** They cannot run on one vault at once: `process()` refuses unless its ledger key is the vault's. Switching the vault's ledger key from one to the other leaves two histories, and the new writer's ledger fails checks a and c against the earlier anchors. This follows from the code; no test shows it yet. Give an SDK agent a vault of its own, as [one writer per vault](https://probarion.xyz/docs/concepts/ledger-and-anchors.md#one-writer-per-vault) explains.
- **It has no HTTP API an agent pays through.** The console's invoice API needs a signed-in wallet session. An agent pays from its own process through the SDK, through the MCP server (Live), whose tools take no address and which holds only the agent and ledger keys, or through hosted Payables in the console.
