Skip to content

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.

SurfaceWhere you set it
Autonomous agentagents.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.

FieldTypeMeaning
recipients{label → address}Recipient allowlist. Payment Agent’s send_payment only accepts these.
max_per_tx_nativewei (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_chainstringChain the LLM falls back to when it doesn’t specify one.
allowed_http_domainsstring[]Domain allowlist for the http_fetch tool. Empty = any domain.

Per-agent (session, as_live_*)

Set at session creation. Lives in agent_sessions.

FieldTypeMeaning
allowed_methodsstring[]signMessage / signTypedData / sendTransaction.
allowed_chainsstring[]Empty = any chain the wallet’s curve supports.
allowed_recipientsstring[] | nullnull = no restriction. [] = deny all. Non-empty = strict allowlist.
token_allowances{chain:address → {max_per_tx}}Per-token caps.
max_spend_per_tx_nativeweiCap on native value per tx.
max_spend_total_nativeweiCap on cumulative native spend over the session lifetime.
expires_atRFC3339After 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.

FieldTypeMeaning
blocked_chainsstring[]Always reject. Wins over agent’s allowed_chains.
blocked_recipientsstring[]Always reject. Applies to recipients of both native and token transfers (the address inside the calldata, not the contract).
token_modeallow_all | deny | allow_onlyMode for token transfers.
blocked_tokens[{chain, address}]When token_mode = deny.
allowed_tokens[{chain, address}]When token_mode = allow_only.
max_native_per_tx_capweiOrg-wide ceiling on native value per tx.
max_native_total_capweiOrg-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). A nil cap on either side means “no limit on this axis”. So min(nil, 0.5) = 0.5 and min(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 has allow_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):

  1. Status & wallet. Agent must belong to a TEE-mode org with a usable wallet. Otherwise wallet not found / wallet not TEE-mode.
  2. Org chain block → reject chain_blocked_by_org.
  3. Agent recipient allowlist → reject recipient_not_in_allowlist if the label resolves to nothing.
  4. Org recipient block → reject recipient_blocked_by_org.
  5. 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

  1. Per-tx cap = min(agent.max_per_tx_native, org.max_native_per_tx_cap). If value > captx_value_exceeds_per_tx_limit.

Token path

  1. Org token mode.
    • deny + token in blocked_tokens → reject token_blocked_by_org.
    • allow_only + token not in allowed_tokens → reject token_not_in_org_allowlist.
  2. Per-tx token cap = min(agent.max_per_tx_token[chain:address], org.token_caps[chain:address].max_per_tx). Exceeds → reject token_amount_exceeds_per_tx.

Both paths

  1. RPC + TEE. Fetch chain id / nonce / gas / fees, build the EIP-1559 tx, hash, sign in TEE, broadcast.
  2. Receipt. Poll up to 60 s. The tool returns one of confirmed / reverted / timeout / submitted to 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 ETH
  • token_caps: { "polygon:0x3c499…" (USDC): { max_per_tx: 100000000 /* 100 USDC */ } }

Agent has set:

  • recipients: { David: 0xb0b…, Pedro: 0xpe7… }
  • max_per_tx_native: 1.0 ETH
  • max_per_tx_token: {} (none)
  • default_chain: polygon
CallResultWhy
send_payment(David, USDC, 50) on polygon✅ confirmedRecipient OK, USDC allowed, 50 ≤ min(nil, 100)
send_payment(David, USDT, 5) on polygontoken_blocked_by_orgOrg’s deny list
send_payment(0xdeadbeef…, USDC, 1)recipient_not_in_allowlistNot in agent recipients (the org’s block is the second line of defence anyway)
send_payment(David, native, 0.8) on polygontx_value_exceeds_per_tx_limitmin(1.0, 0.5) = 0.5; 0.8 > 0.5
send_payment(David, USDC, 200) on polygontoken_amount_exceeds_per_txmin(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:

Terminal window
# Autonomous agent runs
GET /v1/orgs/{orgID}/agents/{agentID}/runs/{runID}/events
# Agent session activity
GET /v1/orgs/{orgID}/agent-sessions/{sessionID}/events

Widening 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:

Terminal window
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_total on token allowances/caps is stored but not yet incremented per call. Per-tx caps (the more important guardrail) are enforced reliably.
  • Simulation. No eth_call or trace is performed. If to is a router contract that ends up routing funds elsewhere, the policy engine doesn’t see past the surface call. Use token_mode = allow_only plus 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.