Skip to main content
A machine that pays for things needs money it can reach without asking, and limits it cannot escape. DeviceVault gives each agent a balance in MON and a set of rules that the device key has to pass through on every payment.
The vault is a single shared contract. Each agent has its own account inside it, keyed by agent id. Depositing for agent 2005 never touches agent 2006.

The limits

Every agent account has four controls.

Per-transaction cap

The most a single payment may move. A payment above it reverts with PerTxCap.

Daily cap

The most that may leave in one rolling 24-hour window. Going over reverts with DailyCap.

Recipient allowlist

The only addresses the device may pay. Any other recipient reverts with NotAllowlisted.

Pause

While paused, nothing leaves except an owner withdrawal. Spending reverts with Paused.
The starting caps come from quickstart and travel inside the claim link. The allowlist starts empty, so a freshly claimed device cannot pay anyone until the owner approves a recipient.

How a payment is checked

The device pays with pay(agentId, recipient, amount). It is the only function in the vault that sends value to a third party. Every call runs the same checks, in this order:
1

Caller is the active device

msg.sender must equal KineticRegistry.activeDevice(agentId). That call itself reverts if no device is bound or if the NFT owner changed since binding.
2

Spending is not paused

Otherwise Paused.
3

Amount is positive and the recipient is allowlisted

Otherwise ZeroAmount or NotAllowlisted.
4

Amount fits the per-transaction cap

Otherwise PerTxCap.
5

Amount fits the daily window

If the current window started more than 24 hours ago, a new window starts now with nothing spent. Then spent + amount must not exceed the daily cap, otherwise DailyCap.
6

Balance covers it

Otherwise InsufficientBalance. The balance is debited, the window is charged, and MON is sent.
The daily window is rolling from the first spend, not a calendar day. spentToday(agentId) returns zero once 24 hours have passed since the window started.

Tightening is instant, loosening needs a signature

The asymmetry is deliberate. Making a device safer should be one click. Giving it more power should require proof that the owner, and not a compromised device, asked for it. When the owner calls a loosening function directly, the transaction itself is the authorization and the signature is ignored. When anyone else calls it, they must present an EIP-712 signature from the current owner over one of these structs:
The domain is name = "DeviceVault", version = "1". All three share one counter, loosenNonce[agentId], which increments on each relayed signature, so every signature works once. A personal (eth_sign) signature is rejected. This is how Telegram owner commands work: the device computes the digest with hashLoosenCaps, hashAllowRecipient, or hashUnpause, the owner signs it in their wallet, and the device relays the transaction and pays its gas. The device never gains the ability to sign for the owner.
A cap change must move in one direction. tightenCaps rejects any increase and loosenCaps rejects any decrease, and both reject a change that leaves the caps exactly as they are. To raise one cap and lower the other, make two changes.

Device gas

The device pays for its own transactions in MON, from its own address. Rather than asking the owner to refill it, the device can take gas from its vault with topUpGas(agentId, amount):
  • The transfer goes only to the active device, never anywhere else.
  • It counts against the per-transaction and daily caps like any payment, and it respects the pause.
  • It skips the allowlist, because the recipient is fixed.
  • When the device calls it, the device may never end up holding more than MAX_DEVICE_GAS, which is 5 MON.
The runtime refills automatically. Before each payment or attestation it checks its balance. At or below 1 MON, it computes a top-up that brings it above 1 MON without exceeding the caps, the vault balance, or 5 MON, and sends it first. If the vault cannot cover that, the owner gets a Telegram message with a funding link. See Funding and gas.

Withdrawals

withdraw(agentId, amount) returns MON to the owner. It ignores the caps and works while paused, so an owner can always recover funds from a device they no longer trust. Only the current owner can call it, and the funds go only to that owner.

Rebinding resets the account

When a new device is bound to an agent that already has a vault account, after a revoke or a transfer release, bindLimits treats it as a rebind:
  • the caps are replaced with the new values,
  • the account starts paused,
  • the allowlist is cleared,
  • the daily window is cleared.
The balance stays. A new owner, or a new device, starts from a known safe state and has to unpause deliberately.

Reading the vault

The vault_status tool and the /status command both read these live. Full signatures and events are in the DeviceVault reference.
A plain MON transfer to the vault address reverts. Always deposit with deposit(agentId), which the vault page does for you.