Core Concepts
Decisions & Lifecycle
A decision is a self-contained, encrypted prompt your agent sends to a person. This page covers its shape, its lifetime, the outcomes you can receive, and the polling patterns SDKs use.
The decision payload
A decision is any JSON object your agent defines. Noxy treats the payload as opaque — it is encrypted end-to-end and never inspected by the relay. The shape below is a convention used by the example SDKs and dashboards, not a wire requirement.
{
"kind": "propose_tool_call",
"tool": "transfer_funds",
"args": { "to": "0x…dEaD", "amountWei": "1" },
"title": "Transfer 1 wei to the burn address",
"summary": "The agent is requesting approval to send 1 wei to the burn address."
}The pieces that matter for a good UX are a human-readable title, a short summary, and any structured fields your client app needs to render a meaningful confirmation screen.
TTL — how long a decision lives
Every decision has a time-to-live in seconds (decisionTtlSeconds in the Agent SDK config or ttl_seconds on a per-call basis). Once the TTL elapses without an explicit answer, the relay marks the decision EXPIRED and lets your agent move on.
Default TTL is 10 minutes. Set TTL explicitly to match the workflow. Use short TTLs for synchronous chat-style decisions (30–120 s). Use longer TTLs (hours) for async approvals like overnight runs.
Outcomes
| Outcome | Meaning | What your agent does |
|---|---|---|
APPROVED | The user said yes. | Proceed with the proposed action. |
REJECTED | The user said no. | Stop or take a documented fallback path. |
EXPIRED | TTL elapsed before any device answered. | Treat as a safe default — typically stop, retry later, or escalate. |
PENDING | Still in flight. | Keep polling, or use sendDecisionAndWaitForOutcome to hide the loop. |
Delivery statuses (per device)
When you call RouteDecision, you get back one delivery outcome per device the relay attempted to reach. Delivery is independent of the eventual human outcome — a device can be DELIVERED immediately and the user can still take minutes to answer.
| Status | Meaning |
|---|---|
DELIVERED | The device was online and received the ciphertext over its open stream. |
QUEUED | The device was offline; the decision is durably queued. A wake notification was sent if the device supports it. |
NO_DEVICES | No devices are registered for this identity yet — typically a first-run condition. |
REJECTED | The relay rejected the route (quota exceeded, app suspended, etc.). |
ERROR | A transient relay-side error. Retry with the same request id. |
Polling patterns
Agent SDKs hide most of this behind one call, but it helps to understand what is happening underneath.
One-shot send-and-wait
The simplest path is sendDecisionAndWaitForOutcome(identity, decision, options?). The SDK routes the decision, picks the first decision_id it gets back, and polls until the outcome is terminal or until maxWaitMs elapses.
const resolution = await client.sendDecisionAndWaitForOutcome(
'user@example.com',
decisionPayload,
{ maxWaitMs: 5 * 60_000 } // optional
);Send and poll separately
For background jobs you may not want to hold a connection open. Send the decision, store the decision_id, and poll later — either with the SDK's waitForDecisionOutcome helper or with explicit getDecisionOutcome calls.
const outcomes = await client.sendDecision('user@example.com', decisionPayload);
const decisionId = outcomes.find(o => o.decision_id)?.decision_id;
// …later, from a worker or cron…
const status = await client.getDecisionOutcome({
decisionId,
identityId: 'user@example.com',
});
if (!status.pending) {
// status.outcome is APPROVED, REJECTED, or EXPIRED
}Polling cadence
SDKs use exponential backoff between polls. Defaults across SDKs:
initialPollIntervalMs— 400 msmaxPollIntervalMs— 30 000 msbackoffMultiplier— 1.6maxWaitMs— 900 000 ms (15 minutes)
Override these per call when the workflow demands tighter or looser response windows.
Idempotency
Always pass a stable request_id when you route a decision. If the same request id is retried after a transient failure, the relay returns the existing decision_id instead of creating a duplicate. Combined with retries on your side this gives an at-least-once transport with effectively-once human action.
Tying outcomes back to your workflow
Once you have a terminal outcome, the rest is application logic:
- Approved: execute, write back to your event log, notify downstream systems.
- Rejected: branch to a fallback (e.g. ask a different user, take the safe path).
- Expired: treat as no-decision. Re-issue with a longer TTL or escalate.
See Best Practices for production-ready patterns for EXPIRED, retries, and quotas.