Policy Engine Guide
The Subcent policy engine evaluates every payment request against the agent's active spending policy. It runs a series of checks in a strict order and produces one of three outcomes: auto_approve, request_approval, or reject. This guide explains how each check works and how to design effective policies.
Evaluation Pipeline#
The policy engine runs checks in the following order. If any check fails, evaluation stops immediately and the payment is rejected (or routed for approval, depending on rules). The checks are:
- Budget -- Is the amount within per-transaction, daily, weekly, and monthly limits?
- Category -- Is the payment category allowed?
- Merchant -- Is the merchant allowed?
- Time -- Is the current time within allowed hours and days?
- Velocity -- Has the agent exceeded transaction rate limits?
- Approval Rules -- Do any conditional rules require human review?
Payment Request
|
v
[Budget Check] --fail--> REJECT
|pass
v
[Category Check] --fail--> REJECT
|pass
v
[Merchant Check] --fail--> REJECT
|pass
v
[Time Check] --fail--> REJECT
|pass
v
[Velocity Check] --fail--> REJECT
|pass
v
[Approval Rules] --match "reject"--> REJECT
| --match "request_approval"--> PENDING
|no match
v
AUTO_APPROVE
Budget Check#
The budget check compares the payment amount against configured spending limits.
Configuration Fields#
| Field | Type | Required | Description |
|---|---|---|---|
| max_per_transaction | number | Yes | Maximum USDC for a single payment |
| max_daily | number | No | Maximum USDC spent per day (resets at midnight UTC) |
| max_weekly | number | No | Maximum USDC spent per week (resets Sunday midnight UTC) |
| max_monthly | number | No | Maximum USDC spent per month (resets 1st of month midnight UTC) |
| currency | string | Yes | Must be "USDC" |
How It Works#
The engine loads spending aggregates for the agent (tracked per period) and checks:
amount <= max_per_transactiondaily_spent + amount <= max_daily(if configured)weekly_spent + amount <= max_weekly(if configured)monthly_spent + amount <= max_monthly(if configured)
If any limit is exceeded, the payment is rejected with a message like: "Exceeds max_per_transaction limit of 100 USDC".
Example#
{
"budget": {
"max_per_transaction": 100,
"max_daily": 500,
"max_weekly": 2000,
"max_monthly": 5000,
"currency": "USDC"
}
}
Category Check#
The category check restricts what types of purchases an agent can make.
Configuration Fields#
| Field | Type | Required | Description |
|---|---|---|---|
| mode | string | Yes | "allowlist" (only these categories) or "blocklist" (all except these) |
| list | array | Yes | Array of category strings |
How It Works#
- Allowlist mode: The payment's category must be in the
list. If not, the payment is rejected. - Blocklist mode: The payment's category must NOT be in the
list. If it is, the payment is rejected.
Standard Categories#
groceries, household, electronics, clothing, health, beauty, books, entertainment, api_services, data_services, computing, storage, transportation, food_delivery, subscription
Example#
{
"categories": {
"mode": "allowlist",
"list": ["groceries", "household", "health"]
}
}
Merchant Check#
The merchant check controls which merchants an agent can transact with.
Configuration Fields#
| Field | Type | Required | Description |
|---|---|---|---|
| blocklist | array | Yes | Array of merchant IDs that are blocked |
| allowlist_only | boolean | No | If true, only merchants NOT on the blocklist are permitted |
How It Works#
The engine checks if the target merchant ID is on the blocklist. If it is, the payment is rejected with a message identifying the blocked merchant.
Example#
{
"merchants": {
"blocklist": ["mrc_blocked_vendor_1", "mrc_blocked_vendor_2"],
"allowlist_only": false
}
}
Time Restriction Check#
The time check limits when an agent can make payments.
Configuration Fields#
| Field | Type | Required | Description |
|---|---|---|---|
| allowed_hours | object | No | { "start": "09:00", "end": "17:00" } in 24-hour format |
| timezone | string | Yes | IANA timezone (e.g., "America/New_York", "UTC") |
| allowed_days | array | No | Lowercase day names (e.g., ["monday", "tuesday", "wednesday", "thursday", "friday"]) |
How It Works#
The engine converts the current UTC time to the configured timezone, then checks:
- If
allowed_hoursis set, the current hour must be betweenstartandend - If
allowed_daysis set, the current day of the week must be in the list
Example#
{
"time_restrictions": {
"allowed_hours": { "start": "08:00", "end": "22:00" },
"timezone": "America/New_York",
"allowed_days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
}
}
This restricts the agent to business hours (8 AM - 10 PM ET, weekdays only).
Velocity Check#
The velocity check limits how fast an agent can make transactions, preventing runaway spending.
Configuration Fields#
| Field | Type | Required | Description |
|---|---|---|---|
| max_transactions_per_hour | number | No | Maximum transactions in the current hour |
| max_transactions_per_day | number | No | Maximum transactions in the current day |
| cooldown_after_rejection_seconds | number | No | Seconds the agent must wait after a rejection before trying again |
How It Works#
The engine tracks transaction counts per period using spending aggregates:
hourly_tx_count < max_transactions_per_hourdaily_tx_count < max_transactions_per_day- If
cooldown_after_rejection_secondsis set and the agent was recently rejected, the engine checks if enough time has passed
Spending Aggregation Periods#
| Period | Resets At |
|---|---|
| hourly | Top of each hour |
| daily | Midnight UTC |
| weekly | Sunday midnight UTC |
| monthly | 1st of month midnight UTC |
Example#
{
"velocity_controls": {
"max_transactions_per_hour": 10,
"max_transactions_per_day": 100,
"cooldown_after_rejection_seconds": 300
}
}
Approval Rules#
Approval rules are conditional expressions that can override the default auto_approve outcome. They are evaluated only after all base checks (budget, category, merchant, time, velocity) pass.
Configuration Fields#
Each rule is an object in the approval_rules array:
| Field | Type | Required | Description |
|---|---|---|---|
| condition | string | Yes | Expression to evaluate (e.g., "amount > 100") |
| action | string | Yes | "auto_approve", "request_approval", or "reject" |
| timeout_seconds | number | No | For request_approval: seconds to wait for human decision (default: 3600) |
| fallback | string | No | For request_approval: action if timeout is reached ("reject" or "approve") |
How It Works#
Rules are evaluated in order. The first matching rule determines the outcome:
auto_approve-- Payment proceeds automaticallyrequest_approval-- Payment is held; a notification is sent to the vault owner with anapproval_url. If no decision is made withintimeout_seconds, thefallbackaction is applied.reject-- Payment is rejected with the rule's condition as the reason
If no rules match, the payment is auto-approved.
Example#
{
"approval_rules": [
{
"condition": "amount > 200",
"action": "reject"
},
{
"condition": "amount > 50",
"action": "request_approval",
"timeout_seconds": 3600,
"fallback": "reject"
}
]
}
This policy: rejects all payments over $200, requires human approval for $50-$200, and auto-approves everything under $50.
Escrow Rules#
Escrow rules automatically route high-value payments through escrow for additional protection.
Configuration Fields#
| Field | Type | Required | Description |
|---|---|---|---|
| require_escrow_above | number | No | Payments above this USDC amount are automatically escrowed |
| default_escrow_expiry_hours | number | No | Default expiration time for auto-created escrows |
Example#
{
"escrow_rules": {
"require_escrow_above": 500,
"default_escrow_expiry_hours": 48
}
}
Three Outcomes#
Every policy evaluation produces one of three results:
auto_approve#
All checks passed and no approval rules triggered. The payment is executed on-chain immediately.
{
"decision": "auto_approve",
"checks": {
"budget_per_tx": "pass",
"budget_daily": "pass",
"budget_monthly": "pass",
"category": "pass",
"merchant": "pass",
"time_restriction": "pass",
"velocity": "pass"
}
}
request_approval#
All base checks passed, but an approval rule matched. The payment is held for human review.
{
"decision": "request_approval",
"reason": "amount > 50",
"timeout_seconds": 3600,
"fallback": "reject",
"checks": {
"budget_per_tx": "pass",
"budget_daily": "pass",
"budget_monthly": "pass",
"category": "pass",
"merchant": "pass",
"time_restriction": "pass",
"velocity": "pass"
}
}
reject#
A check failed or an approval rule explicitly rejected the payment.
{
"decision": "reject",
"reason": "Exceeds max_per_transaction limit of 100 USDC",
"cooldown_until": "2025-01-15T11:00:00.000Z",
"checks": {
"budget_per_tx": "fail",
"budget_daily": "pass",
"budget_monthly": "pass"
}
}
Common Policy Patterns#
Conservative Agent (Low Trust)#
For a new or untested agent with tight controls:
{
"budget": { "max_per_transaction": 10, "max_daily": 50, "currency": "USDC" },
"categories": { "mode": "allowlist", "list": ["api_services"] },
"velocity_controls": { "max_transactions_per_hour": 5, "max_transactions_per_day": 20 },
"approval_rules": [
{ "condition": "amount > 5", "action": "request_approval", "timeout_seconds": 1800, "fallback": "reject" }
]
}
Shopping Assistant (Medium Trust)#
For an agent that shops across multiple categories:
{
"budget": { "max_per_transaction": 100, "max_daily": 500, "max_monthly": 3000, "currency": "USDC" },
"categories": { "mode": "allowlist", "list": ["groceries", "household", "electronics", "clothing"] },
"velocity_controls": { "max_transactions_per_hour": 10, "max_transactions_per_day": 50 },
"approval_rules": [
{ "condition": "amount > 50", "action": "request_approval", "timeout_seconds": 3600, "fallback": "reject" }
],
"escrow_rules": { "require_escrow_above": 200, "default_escrow_expiry_hours": 72 }
}
Infrastructure Agent (High Trust)#
For a trusted agent managing API subscriptions:
{
"budget": { "max_per_transaction": 500, "max_daily": 2000, "max_monthly": 10000, "currency": "USDC" },
"categories": { "mode": "allowlist", "list": ["api_services", "computing", "storage", "data_services"] },
"time_restrictions": { "timezone": "UTC" },
"velocity_controls": { "max_transactions_per_day": 200 }
}