On this page
Events
The vault and factory contracts log a NEP-297 event line as their state changes. Read the lines from each contract's own receipts: Probarion sends no webhooks.
The vault and factory contracts log a NEP-297 event line as they change: a mandate created, a spend held or paid, a key added. This page lists every event, the method that logs it and its fields, and shows how to read the lines from the chain. A test pins the tables to the contract source, so an event added, removed or changed in the code fails the docs tests until this page follows.
The line format#
Each event is one log line: the prefix EVENT_JSON:, then a JSON object with four keys. near-sdk writes the line from a Rust enum. The vault marks its enum with #[near(event_json(standard = "probarion"))] and each variant with #[event_version("1.0.0")], and the variant's fields become data. The factory's enum works the same way, under its own standard.
| Key | Vault contract | Factory contract | Meaning |
|---|---|---|---|
standard | probarion | probarion_factory | The family of events. Filter on it together with the account that logged the line. |
version | 1.0.0 | 1.0.0 | The version of the event's shape. Every event of both contracts is at this one. |
event | A name from the vault events table | A name from the factory events table | The Rust variant's name in snake_case. |
data | One JSON object | One JSON object | The variant's fields, in the order the tables list them. An object, not the array that NEP-141 events use. |
In TypeScript, @probarion/core exports the standards and the version as EVENT_STANDARD, FACTORY_EVENT_STANDARD and EVENT_VERSION.
A real line#
This line was logged by the testnet vault v0.probarion.testnet, in the callback receipt of transaction 3DFV2thBU9V4b5w7kM691poLJ8SWiCqp46LKBn1Fy9cX. That transaction is the 1 USDC payment that contracts/deployments/testnet.json records as near_payment_tx. This first vault runs a build older than the current source (see Testnet and mainnet), and its spend_paid line has the same fields as the source's.
EVENT_JSON:{"standard":"probarion","version":"1.0.0","event":"spend_paid","data":{"spend_id":1,"mandate_id":1,"amount":"1000000","destination":{"type":"near","account_id":"probarion-payee.testnet"}}}The same object, spread out:
{
"standard": "probarion",
"version": "1.0.0",
"event": "spend_paid",
"data": {
"spend_id": 1,
"mandate_id": 1,
"amount": "1000000",
"destination": { "type": "near", "account_id": "probarion-payee.testnet" }
}
}The contract writes the keys in this order, with no spaces. Parse the JSON anyway, and never match the text.
Vault events#
Every vault account logs these, under the standard probarion. "Emitted by" names the method whose receipt logs the line and, when another function builds it, that function too. "Data fields" gives each field with its Rust type, in the order the line carries them. Field types shows how each type looks in the JSON.
| Event | Emitted by | Data fields | Meaning |
|---|---|---|---|
mandate_created | create_mandate | mandate_id: u64, payee: Payee, max_total: U128, max_per_payment: U128, expires_at_ns: U64, purpose_hash: String, agent_pk: PublicKey | The owner stored a new mandate. The line leaves out max_uses, approval_above and label: read them with get_mandate. |
mandate_revoked | revoke_mandate | mandate_id: u64 | The owner revoked the mandate. It refuses new spends and approvals from now on. A payout already sent still settles. |
agent_added | add_agent | public_key: PublicKey, label: Option<String> | The owner registered an agent key. label is always set here. The key itself is added, limited to spend, in a separate receipt that can still fail. |
agent_removed | remove_agent | public_key: PublicKey, label: Option<String> | The owner removed an agent key. label is always set: the removed key's label. The key is deleted in a separate receipt. |
ledger_key_set | set_ledger_key | public_key: PublicKey | The owner set the ledger key, which may call only anchor. Deleting the previous key and adding this one run as two separate receipts. |
spend_held | spend, through hold | spend_id: u64, mandate_id: u64, amount: U128, invoice_hash: String, decision_hash: String, held_until_ns: U64 | The amount is above the mandate's approval_above. Nothing is sent. The amount stays reserved until an approver rejects it, the approved payout settles or fails, or someone calls expire_held after held_until_ns. |
spend_submitted | spend or approve, through execute_payout | spend_id: u64, mandate_id: u64, amount: U128, invoice_hash: String, decision_hash: String, destination: Payee | The vault started the payout to the mandate's payee. Not final: spend_paid, spend_burned or spend_failed follows in the callback receipt. |
spend_approved | approve | spend_id: u64, by: AccountId | An approver approved a held spend. spend_submitted follows in the same receipt. |
spend_rejected | reject | spend_id: u64, by: AccountId | An approver rejected a held spend. The reservation is released, and the spend's ref stays used. |
spend_expired | expire_held | spend_id: u64 | Someone released a hold after its window ended: anyone may call expire_held. The reservation is released, and the ref stays used. |
spend_paid | on_transfer, through settle | spend_id: u64, mandate_id: u64, amount: U128, destination: Payee | The USDC transfer to the NEAR payee succeeded. The amount counts as spent, and the spend is final. |
spend_burned | on_withdraw, through settle | spend_id: u64, mandate_id: u64, amount: U128, chain: Chain, address: String, asset: AccountId, memo: String | The withdrawal through NEAR Intents neither failed nor returned "0", so the vault counts the amount as spent toward address on chain. memo is WITHDRAW_TO: and the address. The release on that chain is not in this event. |
spend_failed | on_transfer or on_withdraw, through fail_spend | spend_id: u64, mandate_id: u64, amount: U128, reason: String | The payout did not go through, and the reservation is released. reason is ft_transfer_failed, ft_withdraw_failed or ft_withdraw_refunded. |
frozen | freeze | by: AccountId | The owner froze the vault. Spends and approvals are refused until it is unfrozen. Reject and expiry still work. |
unfrozen | unfreeze | by: AccountId | The owner unfroze the vault. |
ledger_anchored | anchor | seq: u64, root: String, count: u64 | The ledger key anchored the off-chain ledger. seq goes up by one each time, count is the number of entries covered, and root is the link of the last of them. |
withdrawn | withdraw or withdraw_intents | to: AccountId, token: AccountId, amount: U128 | The owner asked for amount of token back to to, the owner's account. It is logged before the transfer runs, and its receipt shows SuccessReceiptId even when the transfer then fails: follow that receipt id to the token's or NEAR Intents' receipt, or read the transaction's final status. |
The signer differs by method. An agent key signs spend as the vault itself, the ledger key signs anchor, and anyone may call expire_held. The callbacks on_transfer and on_withdraw run as the vault, after the payout's own receipt.
Owner signs
The owner signs the calls behind the mandate, key, freeze and withdraw events from their own wallet: create_mandate, revoke_mandate, add_agent, remove_agent, set_ledger_key, freeze, unfreeze, withdraw and withdraw_intents. The owner also signs the factory's create_vault, with the vault's deposit.
Approver signs
An approver signs approve and reject, the calls behind spend_approved and spend_rejected, from their own wallet.
Factory events#
The factory account logs these, under the standard probarion_factory. On testnet the factory is vaults.probarion.testnet, and a vault it creates is <name>.vaults.probarion.testnet.
| Event | Emitted by | Data fields | Meaning |
|---|---|---|---|
vault_created | on_vault_created, the callback of create_vault | account_id: AccountId, owner: AccountId, version: u32 | The factory created and initialised the vault account and recorded it. When the batch failed, nothing is logged and the deposit goes back to the owner. |
version_added | add_version | index: u32, code_hash: String, mode: String | The factory owner added a vault code version, which new vaults now get. mode is global or embedded, and code_hash is base58. |
Field types#
| Rust type | In the line | Example |
|---|---|---|
u64 | A JSON number: an id, a seq or a count. | 1 |
u32 | A JSON number. | 1 |
U128 | A decimal string, so no precision is lost. Amounts are in the token's smallest unit: 6 decimals for USDC. | "1000000", which is 1 USDC |
U64 | A decimal string. The _ns fields are nanoseconds since the Unix epoch, on the block clock. | "1794182400000000000" |
String | A JSON string. The vault's _hash fields and root are 64 lowercase hex characters, and the factory's code_hash is base58. | "8f94c0bb..." |
Option<String> | A string, or null. The vault always sets it today. | "research agent" |
AccountId | A NEAR account id, as a string. | "v0.probarion.testnet" |
PublicKey | A public key as a string, with its curve first. | "ed25519:..." |
Payee | An object tagged by type: near with account_id, or intents with chain, address and asset. | {"type":"near","account_id":"probarion-payee.testnet"} |
Chain | A string: base or solana. | "base" |
Read events from the chain#
The lines live in the receipts each contract executes. To read them:
- Get the receipts. An indexer that follows the vault account gives you every one, and the RPC
txmethod gives the receipts of one transaction you know. - Keep only the receipts that succeeded, whose status is
SuccessValueorSuccessReceiptId. A receipt that failed keeps the logs it wrote before it failed, but its changes were undone. Awithdrawnline needs one more check, because its receipt succeeds before the transfer runs: see its row. - Keep only the receipts whose
executor_idis the vault, or the factory for its events. Any contract can log a line that says"standard":"probarion". The factory'svault_createdevents and itsvault_versionview tell you which accounts it created. - Take each log line that starts with
EVENT_JSON:, parse the rest as JSON, and checkstandardandversion. - Look up
eventin the tables above, and readdata.
This script does that for the payment above. It uses only the fetch built into Node.js, and it asks FastNEAR's archival RPC, because a regular node drops old transactions.
const RPC = "https://archival-rpc.testnet.fastnear.com";
const VAULT = "v0.probarion.testnet";
const TX = "3DFV2thBU9V4b5w7kM691poLJ8SWiCqp46LKBn1Fy9cX";
const STANDARD = "probarion";
const response = await fetch(RPC, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tx",
params: { tx_hash: TX, sender_account_id: VAULT, wait_until: "FINAL" },
}),
});
const { result } = await response.json();
for (const receipt of result.receipts_outcome) {
const { executor_id, status, logs } = receipt.outcome;
const succeeded =
typeof status === "object" && ("SuccessValue" in status || "SuccessReceiptId" in status);
if (executor_id !== VAULT || !succeeded) continue;
for (const line of logs) {
if (!line.startsWith("EVENT_JSON:")) continue;
const event = JSON.parse(line.slice("EVENT_JSON:".length));
if (event.standard !== STANDARD) continue;
console.log(event.event, event.data.spend_id);
}
}Save it as events.mjs and run node events.mjs. It prints the two vault events of that payment. The USDC contract's own ft_transfer line sits in another account's receipt, so the script skips it.
spend_submitted 1
spend_paid 1Events tell you what changed and when. The views tell you how things stand now: get_spend and list_spends, get_mandate and list_mandates, and list_anchors. After a gap in your indexer, read the views to catch up. For offline tests, the SDK's test doubles in @probarion/sdk/testing log the spend events and ledger_anchored in the same shape.
The hosted watcher#
The hosted app does not read these lines. Its watcher polls the views list_spends, list_mandates and list_anchors of each vault registered with the app, reads the ledger the app keeps in its database, reads the vault's access keys and NEAR balance for the low-allowance and low-NEAR alerts, and stores what it sees for the usage counts and the owner's alerts. A vault that only the SDK or the CLI drives, and that nobody registered, is not watched. The watcher is Preview: see What works today. The code is in apps/web/src/lib/watcher/watch.ts (opens in a new tab).
No webhooks#
Probarion has no outbound webhooks and no event subscription API. Nothing calls your server when a vault logs an event. To follow a vault, index its receipts or poll its views, as above. The only webhook route in the app is inbound: it receives the Telegram bot's updates for alerts (see What works today).
What the events do not cover#
- Refusals. A refused call panics with
PROBARION:<Code>, so its receipt fails and nothing it logged counts. The codes are on Errors. - The clock. No event marks a mandate that expires or is used up, or a hold whose window ends. A hold becomes
spend_expiredonly when someone callsexpire_held: see Release an expired hold. - Refs and agent keys. No spend event carries the spend's
refor the agent key that signed it. Read them withget_spend. - Setup. The
newinit of the vault and of the factory logs nothing, and neither do the factory'sstore_codeandpublish_global. - The other chain.
spend_burnedends at NEAR Intents. The release on Base or Solana is not a Probarion event. On testnet a stub stands in for NEAR Intents, and nothing leaves NEAR. - The ledger itself. Ledger entries live off chain, and only their anchors are logged: see Anchors: pinning the ledger on chain.
- Token events. The token contracts and NEAR Intents log their own NEP-141 and NEP-245 lines in their own receipts. The verifier reads the burn lines for check h.
Related pages#
- Contract reference: the methods of the vault and the factory that log these events.
- Holds and approvals: the life of a held spend, behind
spend_held,spend_approved,spend_rejectedandspend_expired. - The ledger and anchors: what
ledger_anchoredpins. - Testnet and mainnet: the factory and vault accounts on each network.
- The sources:
contracts/vault/src/events.rs(opens in a new tab) andcontracts/factory/src/lib.rs(opens in a new tab).
Built from 4ab41ca · reviewed
contracts/vault/src/events.rs(opens in a new tab)contracts/vault/src/mandates.rs(opens in a new tab)contracts/vault/src/keys.rs(opens in a new tab)contracts/vault/src/held.rs(opens in a new tab)contracts/vault/src/spend.rs(opens in a new tab)contracts/vault/src/admin.rs(opens in a new tab)contracts/vault/src/anchors.rs(opens in a new tab)contracts/vault/src/types.rs(opens in a new tab)contracts/vault/src/checks.rs(opens in a new tab)contracts/vault/src/validate.rs(opens in a new tab)contracts/vault/src/lib.rs(opens in a new tab)contracts/vault/src/errors.rs(opens in a new tab)contracts/vault/src/ext.rs(opens in a new tab)contracts/intents-stub/src/lib.rs(opens in a new tab)contracts/factory/src/lib.rs(opens in a new tab)contracts/deployments/testnet.json(opens in a new tab)apps/web/test/fixtures/testnet-spend-trees.json(opens in a new tab)apps/web/src/lib/watcher/watch.ts(opens in a new tab)apps/web/src/lib/watcher/server.ts(opens in a new tab)packages/core/src/constants.ts(opens in a new tab)packages/core/src/index.ts(opens in a new tab)packages/sdk/package.json(opens in a new tab)packages/sdk/src/snapshot.ts(opens in a new tab)packages/sdk/src/testing/fake-chain.ts(opens in a new tab)apps/web/src/app/api/telegram/webhook/route.ts(opens in a new tab)docs/ops/watcher.md(opens in a new tab)