Skip to content

SENTINEL REST API

All endpoints live under /api/sentinel. JWT-based auth via Authorization: Bearer <token>.

  • Public routes require verifyJwt only.
  • Admin routes require verifyJwt + requireOwner — wallet must appear in AUTHORIZED_WALLETS env var.

All SENTINEL routes that emit errors return a structured envelope:

{ "error": { "code": "<CODE>", "message": "<human-readable>" } }

| Code | HTTP status | Meaning | |---|---|---| | VALIDATION_FAILED | 400 | Required fields missing or malformed | | NOT_FOUND | 404 | Resource ID does not exist | | FORBIDDEN | 403 | Reserved (auth middleware emits its own legacy shape today) | | UNAVAILABLE | 503 | Server up but a dependency is unconfigured | | INTERNAL | 500 | Unexpected server-side failure |

Note on auth errors: 401/403 responses from verifyJwt and requireOwner middleware still use the legacy {error: "string"} shape. Normalizing those is tracked separately as a future API-wide cleanup.


Auth: verifyJwt

Description: Run a one-shot risk assessment for a proposed action. Used by external integrators or internal callers that want a verdict without committing to execution. The SENTINEL assessor (a Pi-SDK LLM agent) evaluates the action context and returns a structured RiskReport.

Request body:

{
"action": "string (required)",
"wallet": "string (required)",
"recipient": "string (optional)",
"amount": "number (optional)",
"token": "string (optional)",
"metadata": "object (optional)"
}

Response 200RiskReport:

{
"risk": "high",
"score": 100,
"reasons": [
"SENTINEL output failed schema validation"
],
"recommendation": "block",
"blockers": [
"schema-violation"
],
"decisionId": "01KQP24PG0KZCJVWJDQM8H3JAY",
"durationMs": 440
}

Response 400:

{ "error": { "code": "VALIDATION_FAILED", "message": "action and wallet are required strings" } }

Returned when action or wallet is missing or not a string.

Response 500:

{ "error": { "code": "INTERNAL", "message": "<error message from assessor>" } }

Returned when the assessor throws an unexpected error.

Response 503:

{ "error": { "code": "UNAVAILABLE", "message": "SENTINEL assessor not configured" } }

Only returned when no assessor is registered at startup. Production agents always register one via setSentinelAssessor, so this path is rarely hit in deployed environments.

Example:

Terminal window
curl -X POST http://localhost:5006/api/sentinel/assess \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"action": "vault_refund",
"wallet": "C1phrE76Wrkmt1GP6Aa9RjCeLDKHZ7p4MPVRuPa8x85N",
"amount": 1.5
}'

Auth: verifyJwt

Description: List blacklisted addresses. Soft-removed entries (where removed_at is set) are filtered out by default.

Query params:

| Param | Type | Default | Description | |-------|------|---------|-------------| | limit | number | 50 | Maximum entries to return |

Response 200:

{
"entries": []
}

Example:

Terminal window
curl -H "Authorization: Bearer $JWT" \
"http://localhost:5006/api/sentinel/blacklist?limit=50"

Auth: verifyJwt

Description: List queued circuit-breaker pending actions (SQLite-backed). Optionally filter by wallet or status.

Query params:

| Param | Type | Default | Description | |-------|------|---------|-------------| | wallet | string | — | Filter by originating wallet address | | status | string | — | Filter by action status — lifecycle is pendingexecutingexecuted | cancelled |

Response 200:

{
"actions": []
}

Example:

Terminal window
curl -H "Authorization: Bearer $JWT" \
"http://localhost:5006/api/sentinel/pending?wallet=C1phrE76Wrkmt1GP6Aa9RjCeLDKHZ7p4MPVRuPa8x85N&status=pending"

Auth: verifyJwt

Description: SENTINEL runtime status snapshot — current mode, preflight scope, configured model, daily LLM cost accrued, and daily budget ceiling.

Response 200:

{
"mode": "advisory",
"preflightScope": "fund-actions",
"model": "openrouter:anthropic/claude-sonnet-4.6",
"dailyBudgetUsd": 10,
"dailyCostUsd": 0,
"blockOnError": false
}

| Field | Type | Description | |-------|------|-------------| | mode | "yolo" | "advisory" | "off" | Current operating mode | | preflightScope | string | Which tool categories trigger the preflight gate | | model | string | LLM model identifier used for assessments | | dailyBudgetUsd | number | Maximum daily spend allowed for LLM calls | | dailyCostUsd | number | Actual spend accumulated today (UTC) | | blockOnError | boolean | Whether assessment errors cause the action to be blocked |

Example:

Terminal window
curl -H "Authorization: Bearer $JWT" \
http://localhost:5006/api/sentinel/status

Admin routes require both verifyJwt AND requireOwner middleware. The calling wallet must appear in the AUTHORIZED_WALLETS environment variable (comma-separated list).

Auth: verifyJwt + requireOwner

Description: Insert a new blacklist entry. The added_by field is set to admin:<wallet> using the verified wallet from the JWT, or admin if no wallet is present.

Request body:

{
"address": "string (required)",
"reason": "string (required)",
"severity": "string (required)",
"expiresAt": "string (optional, ISO 8601 timestamp)",
"sourceEventId": "string (optional)"
}

Response 200:

{
"success": true,
"entryId": "01KQP25BM8RVZJ92CTANFDNTMG"
}

entryId is a ULID generated by the SQLite insert.

Response 400:

{ "error": { "code": "VALIDATION_FAILED", "message": "address, reason, severity required" } }

Example:

Terminal window
curl -X POST http://localhost:5006/api/sentinel/blacklist \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"address": "BadActor11111111111111111111111111111111111",
"reason": "Flagged by SENTINEL risk assessment",
"severity": "high"
}'

Auth: verifyJwt + requireOwner

Description: Soft-remove a blacklist entry. Sets removed_at, removed_by, and removed_reason columns. The row is not deleted from SQLite — it is retained for audit purposes and filtered from GET /blacklist responses.

Path params:

| Param | Type | Description | |-------|------|-------------| | id | string | ULID of the blacklist entry to remove |

Request body (optional):

{ "reason": "string (default: 'manual removal')" }

Response 200:

{
"success": true
}

Example:

Terminal window
curl -X DELETE http://localhost:5006/api/sentinel/blacklist/01KQP25BM8RVZJ92CTANFDNTMG \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{ "reason": "false positive, cleared by review" }'

POST /api/sentinel/circuit-breaker/:id/cancel

Section titled “POST /api/sentinel/circuit-breaker/:id/cancel”

Auth: verifyJwt + requireOwner

Description: Cancel a circuit-breaker pending action (SQLite-backed). Calls cancelCircuitBreakerAction in packages/agent/src/sentinel/circuit-breaker.ts. The cancelled_by field is set to user:<wallet> from the JWT, or admin if unavailable.

Path params:

| Param | Type | Description | |-------|------|-------------| | id | string | ULID of the circuit-breaker pending action |

Request body (optional):

{ "reason": "string (default: 'manual cancel')" }

Response 200:

{ "success": true }

Returned when the action ID exists and was cancelled.

Response 404:

{ "error": { "code": "NOT_FOUND", "message": "pending action not found or already resolved" } }

Returned when the action ID does not exist or is already settled. A missing or already-resolved action returns this 404 envelope (not 200 + {success: false}).

Example:

Terminal window
curl -X POST http://localhost:5006/api/sentinel/circuit-breaker/01KQP25BM8RVZJ92CTANFDNTMG/cancel \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{ "reason": "admin override, action no longer needed" }'

Auth: verifyJwt + requireOwner

Description: List recent SENTINEL decisions — the assessment outcomes recorded each time SENTINEL evaluated an action. Optionally filter by source.

Query params:

| Param | Type | Default | Description | |-------|------|---------|-------------| | limit | number | 50 | Maximum decisions to return | | source | string | — | Filter by source label (e.g., agent, scanner, assess) |

Response 200:

{
"decisions": []
}

Example:

Terminal window
curl -H "Authorization: Bearer $JWT" \
"http://localhost:5006/api/sentinel/decisions?limit=20&source=agent"

Auth: verifyJwt + requireOwner

Description: Return the current SENTINEL configuration snapshot — operating mode, preflight scope/thresholds, blacklistAutonomy, rate limits, scan intervals, autoRefundThreshold, model, dailyBudgetUsd, accrued dailyCostUsd, blockOnError, and the fundMovingTools list. See Configuration for what each field controls.

Response 200:

{
"mode": "advisory",
"preflightScope": "fund-moving",
"blacklistAutonomy": true,
"model": "openrouter:anthropic/claude-sonnet-4.6",
"dailyBudgetUsd": 10,
"dailyCostUsd": 0,
"blockOnError": false,
"fundMovingTools": ["executeRefund", "addToBlacklist"]
}

Example:

Terminal window
curl -H "Authorization: Bearer $JWT" \
http://localhost:5006/api/sentinel/config

POST /api/sentinel/promise-gate/:flagId/resolve

Section titled “POST /api/sentinel/promise-gate/:flagId/resolve”

Auth: verifyJwt + requireOwner

Description: Resolve a pending in-memory promise-gate flag — the admin approval path when SENTINEL is in advisory mode and has paused an action for human review. Calls resolvePending(flagId) in packages/agent/src/sentinel/pending.ts.

Path params:

| Param | Type | Description | |-------|------|-------------| | flagId | string | ID of the in-memory pending promise-gate flag |

Response 204: No body — flag was found and resolved. The paused agent tool call proceeds.

Response 404:

{ "error": { "code": "NOT_FOUND", "message": "flag not found or expired" } }

Returned when no in-memory flag exists for the given flagId. Flags expire when the waiting agent times out.

Example:

Terminal window
curl -X POST http://localhost:5006/api/sentinel/promise-gate/flag_01KQP24PG0KZCJVWJDQM8H3JAY/resolve \
-H "Authorization: Bearer $JWT"

POST /api/sentinel/promise-gate/:flagId/reject

Section titled “POST /api/sentinel/promise-gate/:flagId/reject”

Auth: verifyJwt + requireOwner

Description: Reject a pending in-memory promise-gate flag — the admin denial path when SENTINEL is in advisory mode and has paused an action. Calls rejectPending(flagId, 'cancelled_by_user') in packages/agent/src/sentinel/pending.ts. The paused tool call receives a rejection and the agent aborts the action.

Path params:

| Param | Type | Description | |-------|------|-------------| | flagId | string | ID of the in-memory pending promise-gate flag |

Response 204: No body — flag was found and rejected. The paused agent tool call receives a rejection error.

Response 404:

{ "error": { "code": "NOT_FOUND", "message": "flag not found or expired" } }

Returned when no in-memory flag exists for the given flagId.

Example:

Terminal window
curl -X POST http://localhost:5006/api/sentinel/promise-gate/flag_01KQP24PG0KZCJVWJDQM8H3JAY/reject \
-H "Authorization: Bearer $JWT"