An Intercept integration can fail in two different ways, and they need different handling. A decision is Intercept working correctly and telling you not to proceed. A transport error is Intercept, the network or your own request failing before a decision is reached. Confusing the two is the most common way to build an integration that fails open.
Decisions versus transport errors
| Decision | Transport error | |
|---|---|---|
| HTTP status | 200 |
4xx, 5xx, timeout, connection failure |
| Body | Decision object with reasons |
Error object or nothing |
| Meaning | Intercept evaluated the transaction | No evaluation happened |
| Retry? | No; the same transaction will get the same answer | Sometimes; see below |
| Default handling | Respect the decision | Fail closed: treat as blocked |
A blocked decision is not an exception. The SDK is designed to return it as a normal value and reserve thrown errors for transport failures, misuse (such as calling execute on a non-approved result) and invalid configuration.
Reason codes
| Code | Layer | Meaning | Recommended agent behavior |
|---|---|---|---|
RISK_MAX_TX_SIZE |
risk | Transaction value exceeds maxTransactionUsd |
Split into smaller transactions only if the strategy allows; do not loop to evade the limit |
RISK_DAILY_LIMIT |
risk | Rolling 24h outflow would exceed dailySpendLimitUsd |
Stop new outflows until the window rolls; alert the operator |
RISK_MAX_LEVERAGE |
risk | Resulting leverage exceeds maxLeverage |
Reduce size or add collateral, then re-evaluate |
RISK_POSITION_SIZE |
risk | Position would exceed maxPositionPct of portfolio |
Reduce size |
RISK_DRAWDOWN |
risk | Drawdown in window exceeds maxDrawdownPct |
Stop increasing exposure; expect circuit breakers |
RISK_SLIPPAGE |
risk | Expected slippage exceeds maxSlippageBps |
Re-quote, reduce size or wait for liquidity |
RISK_LIQUIDITY |
risk | A routed pool is below minPoolLiquidityUsd |
Choose a different route |
SEC_PROTOCOL_NOT_ALLOWED |
security | Transaction invokes a program not in allowedProtocols |
Do not retry; a human must change the policy |
SEC_BLOCKED_ADDRESS |
security | Transaction touches an address in blockedAddresses |
Do not retry; treat as a security event |
SEC_SIMULATION_FAILED |
security | Simulation failed or was unavailable under simulation: "required" |
Rebuild with a fresh blockhash and re-evaluate once; then stop |
SEC_UNLIMITED_APPROVAL |
security | Transaction grants an unbounded token delegate | Rebuild with an exact approval amount |
SEC_AUTHORITY_CHANGE |
security | Transaction changes an account, mint or upgrade authority | Do not retry; requires human review |
SEC_PROMPT_INJECTION_SUSPECTED |
security | Stated intent does not match simulated effects | Halt the current task, discard untrusted inputs, alert the operator |
NET_FLAGGED_ADDRESS |
network | An address, authority or mint is flagged in the Threat Registry | Do not retry; see Threat Intelligence |
NET_FLAGGED_PROGRAM |
network | A program is flagged in the Threat Registry | Do not retry |
SYS_PAUSED |
system | Agent is paused manually, by guardian or by circuit breaker | Stop; wait for agent.resumed |
SYS_TIMEOUT |
system | Evaluation did not complete within timeoutMs |
Fail closed; retry with the same idempotency key |
SYS_POLICY_INVALID |
system | Active policy is missing or failed validation | Stop; fix and republish the policy |
The NET_* codes depend on the proposed Intercept Network, which is planned for a later phase than the other layers.
An agent should never treat a security or network code as a puzzle to route around. Retrying a blocked transaction with small changes until it passes is the behavior Intercept exists to stop. In advisory mode, nothing technically prevents it, which is why blocking only works if the integration is enforced.
Fail-closed default
When the SDK cannot obtain a decision, it is designed to return a synthetic blocked result with SYS_TIMEOUT rather than throw or approve. This is the failMode: "closed" default.
const result = await intercept.evaluate(transaction, { agentId: "agent_7f3c" });
// On timeout or 5xx, result.decision === "blocked" and
// result.reasons[0].code === "SYS_TIMEOUT"failMode: "open" exists for advisory-mode testing only. It is designed to be rejected when the policy mode is enforced. In enforced mode there is no fail-open option: if Intercept cannot co-sign, the transaction cannot execute.
Retries
| Condition | Retry? | How |
|---|---|---|
200 with any decision |
No | Re-evaluate only after changing the transaction |
400, 401, 403, 404, 422 |
No | Fix the request, key or policy |
409 idempotency conflict |
No | Use a new key for a different request |
429 |
Yes | Wait for Retry-After |
5xx, timeout, connection reset |
Yes | Exponential backoff with jitter, same idempotency key |
Solana transactions also expire. A retry loop that outlasts the blockhash's validity, roughly 150 slots, will produce SEC_SIMULATION_FAILED. Rebuild with a fresh blockhash, or use a durable nonce, and evaluate again with a new idempotency key.
Idempotency keys
Send an Idempotency-Key header (or idempotencyKey in the SDK context) on every POST and PUT. If a request is retried with the same key and body, Intercept is designed to return the original response instead of evaluating or executing twice.
This matters most for execute. If the connection drops after Intercept co-signed and submitted, a naive retry could appear to fail or, worse, submit a second transaction. With the same key, the retry returns the original signature.
- Generate one key per logical operation, such as a UUID, not per HTTP attempt.
- Reusing a key with a different body returns
409. - Keys are designed to be retained for 24 hours.