LIMEN
TESTNET

The authorization path

A boundary is only worth what enforces it. Nothing in this path is Limen: the check runs in contract code on the ledger, and it runs whether or not this repository still exists.

TESTNET ONLYNOT AUDITED

What runs before a token moves

The agent signs an envelope with its own key and pays its own fee. No owner signature is anywhere near it. The network invokes __check_auth on the smart account, and the account’s own code decides.

The authorization pathThe agent signs an envelope. The network invokes __check_auth on the smart account, which finds the context rule for that signer, verifies the signature through the ed25519 verifier, and asks each attached policy. If every policy assents the transfer executes; if any refuses the invocation traps and nothing moves.agent signsits own key, its own feenetworkinvokes __check_authSMART ACCOUNT CONTRACT — ON LEDGER, NOT THIS APPLICATIONfind context rulefor this signerverify signatureed25519 verifier contracteach attached policy assertsspending limit · function allowlist✓ every policy assents✕ any policy refuses → traptransfer executesthe token movesnothing movesfee burned, hash on ledger

If any attached policy refuses, the invocation traps. The transaction still reaches a ledger and still burns a fee — which is what makes a refusal checkable by anyone rather than a claim this application makes about itself.

The structure of an account

The relationship between signers, context rules and policies is the thing most often read backwards. A policy is attached to a rule, not to a signer, and a signer’s authority is exactly the union of the rules that name it.

The structure of a smart accountA smart account holds context rules. The owner holds a Default rule that authorizes any call. The agent is registered under a CallContract rule scoped to one token contract, with a spending limit policy attached to that rule. A policy is attached to a rule, not to a signer.SMART ACCOUNTCONTEXT RULE — DEFAULTowner signerAuthorizes any call the account can make.No policy attached — this is the rule revoke needs.CONTEXT RULE — CALLCONTRACTagent signerScoped to one token contract, derived fromthe observed transaction. Nothing wider.POLICY ATTACHED TO THE RULEspending_limitA configured OpenZeppelin primitive. Cap plus window.A policy is attached to a rule, not to a signer.A signer’s authority is the union of the rulesthat name it — which is why removing the agent’srule removes its authority entirely.

Who enforces this

Three deployed contracts, all of them OpenZeppelin code that Limen configures rather than writes. Their addresses are read from the deployments file.

ContractAddressDeployed
ed25519 verifierCA3ZVES4…5HPGZX41c2459f…1e78
WebAuthn verifierCCC4T3F7…2IAKWYf95ff610…be50
spending limit policyCDWPYL45…TH4CSE8935d56a…4fe4

Revoking, and who can

Removing a context rule requires the account to authorize itself. The agent’s rule is a CallContract rule scoped to a token contract, which does not match a call to the account — so the agent’s attempt to remove its own boundary traps. The owner’s Default rule does match.

This is a property of the contract, not of this application declining to draw a button. Both halves are on record:

AttemptOutcomeOn ledger
the agent removing its own ruleUnvalidatedContext#3002fca28f06…ae4e
the owner removing itrules after: 08bfb1cc5…781c
the permitted call, repeated afterwardsContextRuleNotFound#3000631f211b…9ef3

The last row fails because the rule is gone, not because a limit was reached. Those are different claims, and only one of them is evidence the boundary worked — which is why ContextRuleNotFound#3000 is deliberately absent from BOUNDARY_REFUSAL_CODES.

ProvenanceThese three transactions are from node packages/chain/scripts/acceptance.mjs run, recorded 2026-08-05 on smart account CBYJPU…5GKW. Re-read from the chain after the write, not assumed. Rule 1 is gone; the owner's Default rule 0 remains.