Policies
Every Sigil agent — whether it’s an autonomous Payment / Trading / Compliance
agent built at agents.sigilkeys.com or an
as_live_* session minted via the S2S API — signs through the same policy
engine. The engine evaluates two layers on every request, and the stricter
side always wins.
The two layers
1. Per-agent policy
Configured by whoever owns the agent or session.
| Surface | Where you set it |
|---|---|
| Autonomous agent | agents.sigilkeys.com → agent detail → Configuration |
Agent session (as_live_*) | At session creation via POST /v1/s2s/agent-sessions |
This is where you express “what does THIS agent need to do?” — allowed
recipients, the cap you’re comfortable with for its job, which chains it
operates on, which methods (signMessage, sendTransaction, etc.) it can
call. It’s the least-privilege contract for that specific worker.
2. Organisation global rules
Configured at the org level by whoever owns the security/compliance side.
Where: platform.sigilkeys.com → Policies → Global rules.
These rules apply to every agent and every session in the organisation. They are the floor under the per-agent contracts — a dev cannot widen them from their agent’s config, and neither can a leaked session token. Use them for hard ceilings (“nobody in this org ever moves more than 0.5 ETH per tx”) and for security gates (“we don’t touch USDT on Optimism, period”).
The fields
Per-agent (autonomous agent)
Stored in agents.config.
| Field | Type | Meaning |
|---|---|---|
recipients | {label → address} | Recipient allowlist. Payment Agent’s send_payment only accepts these. |
max_per_tx_native | wei (decimal string) | Cap on native value per transaction. |
max_per_tx_token | {chain:address → wei} | Cap on ERC-20 amount per token, per chain. Amounts are in the token’s base units (USDC has 6 decimals → 1500000 = 1.50 USDC). |
default_chain | string | Chain the LLM falls back to when it doesn’t specify one. |
allowed_http_domains | string[] | Domain allowlist for the http_fetch tool. Empty = any domain. |
Per-agent (session, as_live_*)
Set at session creation. Lives in agent_sessions.
| Field | Type | Meaning |
|---|---|---|
allowed_methods | string[] | signMessage / signTypedData / sendTransaction. |
allowed_chains | string[] | Empty = any chain the wallet’s curve supports. |
allowed_recipients | string[] | null | null = no restriction. [] = deny all. Non-empty = strict allowlist. |
token_allowances | {chain:address → {max_per_tx}} | Per-token caps. |
max_spend_per_tx_native | wei | Cap on native value per tx. |
max_spend_total_native | wei | Cap on cumulative native spend over the session lifetime. |
expires_at | RFC3339 | After this every call returns session_expired. |
Organisation global rules
One row per org in org_agent_rules. Read by both the agent runner and the
session middleware on every signing call.
| Field | Type | Meaning |
|---|---|---|
blocked_chains | string[] | Always reject. Wins over agent’s allowed_chains. |
blocked_recipients | string[] | Always reject. Applies to recipients of both native and token transfers (the address inside the calldata, not the contract). |
token_mode | allow_all | deny | allow_only | Mode for token transfers. |
blocked_tokens | [{chain, address}] | When token_mode = deny. |
allowed_tokens | [{chain, address}] | When token_mode = allow_only. |
max_native_per_tx_cap | wei | Org-wide ceiling on native value per tx. |
max_native_total_cap | wei | Org-wide ceiling on cumulative native per session (sessions only). |
token_caps | {chain:address → {max_per_tx, max_total}} | Org-wide per-token ceilings. |
Combination rules
Every limit follows the stricter wins principle:
- Caps (numeric) combine by
min(agent_cap, org_cap). Anilcap on either side means “no limit on this axis”. Somin(nil, 0.5) = 0.5andmin(2, 0.5) = 0.5. - Allowlists are ANDed: the address must be in both the agent
allowlist AND not in the org’s
blocked_recipients. - Blocklists are unioned: if the chain is blocked at the org level, it doesn’t matter that the agent’s session allows it.
- Token mode overrides: when org has
deny, the listed tokens are rejected regardless of per-agent config. When org hasallow_only, anything outside the org list is rejected, even if the agent had an allowance for it.
A widening operation that contradicts the org rules is silently bounded by the org rules; it isn’t an error, the request just gets the stricter limit.
Evaluation order
When an autonomous agent calls send_payment(chain, recipient, asset, amount, reason):
- Status & wallet. Agent must belong to a TEE-mode org with a usable wallet. Otherwise
wallet not found/wallet not TEE-mode. - Org chain block → reject
chain_blocked_by_org. - Agent recipient allowlist → reject
recipient_not_in_allowlistif the label resolves to nothing. - Org recipient block → reject
recipient_blocked_by_org. - Asset resolution.
native/eth/matic→ native path. Anything else → look up in the org’s token registry for that chain. Not found →token_not_registered.
Native path
- Per-tx cap =
min(agent.max_per_tx_native, org.max_native_per_tx_cap). Ifvalue > cap→tx_value_exceeds_per_tx_limit.
Token path
- Org token mode.
deny+ token inblocked_tokens→ rejecttoken_blocked_by_org.allow_only+ token not inallowed_tokens→ rejecttoken_not_in_org_allowlist.
- Per-tx token cap =
min(agent.max_per_tx_token[chain:address], org.token_caps[chain:address].max_per_tx). Exceeds → rejecttoken_amount_exceeds_per_tx.
Both paths
- RPC + TEE. Fetch chain id / nonce / gas / fees, build the EIP-1559 tx, hash, sign in TEE, broadcast.
- Receipt. Poll up to 60 s. The tool returns one of
confirmed/reverted/timeout/submittedto the LLM.
For as_live_* sessions the order is the same, just with the session-level
fields (allowed_methods, token_allowances, max_spend_total_native, …)
substituted for the per-agent ones.
Worked example
Org has set:
blocked_chains: []blocked_recipients: ["0xdeadbeef…"]token_mode: deny,blocked_tokens: [{chain: "polygon", address: "0xc213…" /* USDT */}]max_native_per_tx_cap: 0.5 ETHtoken_caps: { "polygon:0x3c499…" (USDC): { max_per_tx: 100000000 /* 100 USDC */ } }
Agent has set:
recipients: { David: 0xb0b…, Pedro: 0xpe7… }max_per_tx_native: 1.0 ETHmax_per_tx_token: {}(none)default_chain: polygon
| Call | Result | Why |
|---|---|---|
send_payment(David, USDC, 50) on polygon | ✅ confirmed | Recipient OK, USDC allowed, 50 ≤ min(nil, 100) |
send_payment(David, USDT, 5) on polygon | ❌ token_blocked_by_org | Org’s deny list |
send_payment(0xdeadbeef…, USDC, 1) | ❌ recipient_not_in_allowlist | Not in agent recipients (the org’s block is the second line of defence anyway) |
send_payment(David, native, 0.8) on polygon | ❌ tx_value_exceeds_per_tx_limit | min(1.0, 0.5) = 0.5; 0.8 > 0.5 |
send_payment(David, USDC, 200) on polygon | ❌ token_amount_exceeds_per_tx | min(nil, 100) = 100; 200 > 100 |
The agent’s owner is free to drop their cap below the org’s (e.g. set
max_per_tx_native: 0.1), and the engine just uses 0.1. They cannot raise
it above the org’s 0.5.
Reading the activity feed
Every signing attempt — allowed or rejected — produces an event. The portal renders them with the chain, target, value, and (for rejections) the reason code. You can also poll the events endpoint from your backend:
# Autonomous agent runsGET /v1/orgs/{orgID}/agents/{agentID}/runs/{runID}/events
# Agent session activityGET /v1/orgs/{orgID}/agent-sessions/{sessionID}/eventsWidening policies
Org rules can be widened or tightened any time from platform.sigilkeys.com → Policies → Global rules. Changes apply on the next signing call (no cache invalidation required — the engine reads the row on every request).
Per-agent caps you can update from the agent’s detail page. Session caps can be widened via:
curl -X PATCH https://api.sigilkeys.com/v1/orgs/ORG_UUID/agent-sessions/SESSION_ID \ -H "Authorization: Bearer sk_live_…" \ -d '{"max_spend_total_native": "5000000000000000000"}'Session narrowing is rejected (narrowing_not_allowed) so a misclick
can’t break a running agent — to tighten a session, revoke it and create a
new one.
What’s still not enforced (Phase 3 candidates)
- Cumulative per-token spend.
max_totalon token allowances/caps is stored but not yet incremented per call. Per-tx caps (the more important guardrail) are enforced reliably. - Simulation. No
eth_callor trace is performed. Iftois a router contract that ends up routing funds elsewhere, the policy engine doesn’t see past the surface call. Usetoken_mode = allow_onlyplus a recipient allowlist to keep agents inside known bounds. - Cross-chain spend ceiling. Limits are per-tx and per-session/per-day, not aggregated across chains.