# Sentinel

> Sentinel, NeuroChain's transaction anomaly scoring — the embedded model and its features, categories, the ingress reject threshold, and the optional policy layer that holds or asks for confirmation instead of rejecting, with trust lists and session-key rules.

Source: https://docs.nro.world/ai/sentinel

Sentinel gives every incoming transfer an **anomaly score** from 0 (normal) to 1 (highly unusual). Depending on settings, the node then accepts it, flags it, refuses it, or — with the policy layer on — holds it for a while or until the owner confirms.

## The model

A small network embedded in the node (8 inputs → 16 → 8 → 1, 289 parameters). Its inputs describe the transfer, not the person:

| Feature | From |
|---------|------|
| Size of the amount (log scale) | `value` |
| Gas price | `gas_price` |
| Nonce gap | `nonce` vs the last confirmed nonce |
| Recipient is new | Prior transfers to the recipient |
| Round amount | `value` |
| Sender velocity | Transfers by the sender in the last 10 blocks |
| Share of balance moved | `value` vs balance |
| Self-transfer | `from == to` |

In consensus the node computes these from **committed blocks only** (velocity is counted from blocks, not from the mempool), so every validator gets the same score for the same transaction.

| Score | Category | `anomalous` |
|-------|----------|-------------|
| < 0.5 | `normal` | no |
| 0.5 – 0.6 | `suspicious` | yes |
| 0.6 – 0.8 | `elevated` | yes |
| ≥ 0.8 | `high_risk` | yes |

Try it without sending anything:

```bash
curl -s -X POST "$GATEWAY/api/v1/sentinel/score" -H 'Content-Type: application/json' \
  -d '{"value":45000000,"gas_price":2,"nonce":3,"last_nonce":2,"balance":48000000,
       "tx_count_last_10":1,"recipient_tx_count":0,"is_self_transfer":false}'
# {"anomalous":true,"category":"elevated","score":0.7119}
```

## What happens to a transfer

![Sentinel decision flow: a score at or above 0.85 is refused at ingress; otherwise, with the policy layer off the transfer applies (flagged if anomalous); with it on, a trusted recipient applies normally, a score at or above 0.70 (or 0.60 for a session key) is held until the owner confirms, a score from 0.60 is held and released automatically after 50 blocks](/diagrams/sentinel-policy.svg)

### Always on <Badge type="tip" text="live" />

- `sentinel_enabled = 1`: every transfer is scored when it reaches a node.
- Score ≥ `sentinel_reject_threshold` (0.85) → refused with HTTP 400.
- Anomalous but below that → accepted; the response carries `sentinel_score`, `sentinel_category` and `sentinel_warning`, and the gateway broadcasts `tx_anomaly`.

### Policy layer <Badge type="warning" text="off by default" />

With `sentinel_policy_enabled = 1`, a transfer that would otherwise apply can be **held**: the sender is debited, the recipient is not credited yet.

| Band | Condition | What happens |
|------|-----------|--------------|
| Trusted | Recipient is on the sender's active trust list | Applied normally |
| Hold | Score ≥ `sentinel_hold_threshold` (0.60) | Released automatically after `sentinel_hold_blocks` (50); the sender may cancel before that |
| Step-up | Score ≥ `sentinel_stepup_threshold` (0.70), **or** signed by a session key with score ≥ `sentinel_agent_confirm_threshold` (0.60) | Waits for the owner to **confirm with the master key**; refunded if not confirmed within `sentinel_confirm_expiry_blocks` (1 000) |

Held transfers have status `held`; they end as `confirmed` (released), `cancelled` or `refunded`. On cancel or refund the value returns to the sender; the fee is kept.

| Action | Endpoint |
|--------|----------|
| List held transfers | `GET /api/v1/held/:address` |
| Confirm (owner, master key) | `POST /api/v1/transactions/:hash/confirm` |
| Cancel (owner) | `POST /api/v1/transactions/:hash/cancel` |

WebSocket events: `tx_held`, `tx_released`, `tx_cancelled`.

### Trust list

An owner can mark recipients as trusted; transfers to them skip the hold. A new entry becomes active only after `trust_activation_blocks` (300) — so a stolen key cannot add an attacker's address and use it at once.

`POST /api/v1/trust/add`, `POST /api/v1/trust/remove`, `GET /api/v1/trust/:owner`.

### Session keys can't approve themselves

Anything aimed at `neuro:system.trust` — trust-list changes, confirming or cancelling a held transfer — is refused for session keys. An AI agent can start a risky transfer; only the owner's master key can let it through. See [Account abstraction](./account-abstraction).

## Calibration <Badge type="warning" text="experimental" />

The model is trained on synthetic transaction patterns. Real transfers score higher than the synthetic data suggested: on a test network a plain 1.5 NRO transfer to a new recipient scores about **0.62**, and measured scores rarely exceed about **0.74**. Two consequences:

- The 0.85 reject threshold is effectively never reached.
- With the default thresholds, turning the policy layer on would hold many ordinary transfers to new recipients.

Thresholds are governance parameters and will be tuned, and the model retrained, on real transaction data before the policy layer is enabled on a public network.

**How it is tested:** the policy layer passed a seven-step test on four validators with a quorum of three — automatic release, step-up confirmation through a different node, refund at expiry, cancellation through a third node, surviving a node restart, trust-list activation, and identical counters and state roots on every node. Node unit tests cover each branch.

## Metrics

Prometheus counters, kept in chain state so every node reports the same numbers: `nc_sentinel_flagged_total{category}`, `nc_sentinel_held_total`, `nc_sentinel_released_total{how}`.
