Protocol
Key confinement
Why the agent holds only a pay-only function-call key and no code, and how to check that yourself.
A spend meter only matters if the agent cannot route around it. On NEAR, an account can hold function-call access keys that are limited to one receiver contract and a list of methods, and that can never attach a deposit. Bound uses that to confine the agent at the protocol level: NEAR itself refuses everything else the key tries, before any contract runs.
How the agent is confined
- The agent accepts its certificate with its full-access key.
- It adds a function-call key with receiver
bound-v3.boundproto-nh26.testnet, methods["pay"]and a 0.25 NEAR gas allowance. - It deletes its full-access key.
The key's method list is ["pay"], so NEAR refuses any other method on Bound, and a function-call key can never attach the 1 yoctoNEAR the other mutating methods require. pay takes no deposit, which is why this key can call it at all. (ft_on_transfer and on_withdraw also take none, but only the token and the contract itself may call them.)
What NEAR refuses, as recorded in act 4 of the demo:
| The key tries | NEAR answers |
|---|---|
ft_transfer on the token (needs 1 yocto) | AccessKeyDepositWithFunctionCallActionError |
mint on the token (no deposit) | AccessKeyReceiverMismatchActionError |
withdraw on Bound (needs 1 yocto) | AccessKeyDepositWithFunctionCallActionError |
| send 0.01 NEAR | AccessKeyRequiresFullAccessActionError |
These produce no transaction at all. Payments the key can make are still checked by the contract: over the float fails with FloatExceeded, off the allowlist with RecipientNotAllowed, after a halt with CertHalted.
Keys are only half of it
Contract code deployed on the agent account runs with the agent as predecessor and needs no key at all. It could call pay, call ft_transfer on the token, or add a full-access key to its own account. An account that uses a NEAR global contract reports a code_hash of all ones, like an account with no code, and names the global code in global_contract_hash or global_contract_account_id; that code runs as the agent too. So confined means all of:
- every access key is a function-call key to the Bound contract with methods exactly
["pay"](and there is at least one key); view_account(agent).code_hashis11111111111111111111111111111111(no contract code);global_contract_hashandglobal_contract_account_idare both null or absent (no global contract).
verify(agent) does not prove confinement
The Bound contract cannot read another account's keys or code. A counterparty reads verify(agent) and both RPC views. The CLI, the SDK's verifyAgent, the MCP tool near_check_agent_keys, the HTTP API and the app all do this for you.
With no full-access key and no code, the key cannot be widened: adding a key needs a full-access key or code on the account. The agent account can never be recovered after its full-access key is deleted. That is the point, and it is why the demo agents are throwaway accounts.
Check it yourself
With near-cli-rs
near account list-keys bound-agent2-mwfnc6q8.testnet network-config testnet now
near account view-account-summary bound-agent2-mwfnc6q8.testnet network-config testnet nowlist-keys should show exactly one key, permitted to only do ["pay"] function calls on bound-v3.boundproto-nh26.testnet. The summary should say Contract: No contract code and 0 full access keys and 1 function-call-only access keys.
With raw JSON-RPC
The summary does not print the global contract fields, so read view_account directly as well:
curl -s https://test.rpc.fastnear.com -H 'content-type: application/json' -d '{
"jsonrpc": "2.0", "id": 1, "method": "query",
"params": {"request_type": "view_access_key_list", "finality": "final",
"account_id": "bound-agent2-mwfnc6q8.testnet"}
}'
curl -s https://test.rpc.fastnear.com -H 'content-type: application/json' -d '{
"jsonrpc": "2.0", "id": 1, "method": "query",
"params": {"request_type": "view_account", "finality": "final",
"account_id": "bound-agent2-mwfnc6q8.testnet"}
}'Expect one key whose permission is FunctionCall with receiver_id bound-v3.boundproto-nh26.testnet and method_names ["pay"], a code_hash of all ones, and no non-null global_contract_* field.
With the Bound tools
npx @bound/near verify bound-agent2-mwfnc6q8.testnet
# confinement: confined (1 key(s), no contract code, no global contract)What the key can still do
- Call
payon any certificate; the contract rejects every one where the caller is not the agent. - Spend its 0.25 NEAR gas allowance on failing calls, which only griefs its own account.
- Pay up to the current float to an allowlisted recipient. This is the residual risk: a hijacked agent can drain the float to an allowlisted counterparty. It is capped by
float_capand stopped by the operator'shalt. With no allowlist, the float can go to any registered account.
See Trust model and limits for what else a certificate does not cover.