Skip to main content

/api/sentinels

CRUD operations for sentinel nodes -- the protocol contracts that BreachResponse monitors for suspicious activity.


Endpoints

MethodPathAuthDescription
GET/api/sentinelsNoList all registered sentinel nodes
POST/api/sentinelsOptionalRegister a new sentinel node
PUT/api/sentinelsOptionalToggle sentinel status (ACTIVE ↔ PAUSED)

GET /api/sentinels

Lists all registered sentinel nodes.

Query Parameters

ParameterTypeRequiredDescription
ownerstringNoFilter by owner wallet address

Response (200)

[
{
"id": "sentinel-ax-node",
"name": "Sentinel.ax Node",
"address": "0x9f758be3ae3D985713964339E2f0bD783fC6015c",
"owner": null,
"status": "ACTIVE",
"latency": "8ms",
"events": 939,
"lastHeartbeat": "2026-06-21T12:00:00.000Z",
"registeredAt": "2026-01-03T00:00:00.000Z"
},
{
"id": "a1b2c3d4-...",
"name": "TargetVault",
"address": "0x9d9b602CFe69cfF9706EAc399808E84682ce94FB",
"owner": "0xabc...",
"status": "ACTIVE",
"latency": "6.4ms",
"events": 42,
"lastHeartbeat": "2026-06-21T12:00:03.000Z",
"registeredAt": "2026-06-21T10:00:00.000Z"
}
]

Sentinel Node Fields

FieldTypeDescription
idstringUnique identifier (UUID or seed string)
namestringHuman-readable name
addressstringContract address (42-char hex)
ownerstring | nullWallet address of the protocol owner
statusstringACTIVE, PAUSED, or OFFLINE
latencystringAgent-to-contract latency (e.g., "6.4ms")
eventsnumberTotal monitored events for this node
lastHeartbeatstringISO-8601 timestamp of last agent heartbeat
registeredAtstringISO-8601 timestamp of registration

Usage

# List all sentinels
curl http://localhost:3000/api/sentinels

# Filter by owner
curl "http://localhost:3000/api/sentinels?owner=0xabc..."

POST /api/sentinels

Registers a new sentinel node for monitoring.

Body

{
"address": "0x9d9b602CFe69cfF9706EAc399808E84682ce94FB",
"name": "TargetVault",
"owner": "0xabc123..."
}
ParameterTypeRequiredDescription
addressstringYesContract address (42-char hex)
namestringNoHuman-readable name (max 200 chars, default: "Custom Sentinel")
ownerstringNoProtocol owner address (max 100 chars)

Response (200)

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "TargetVault",
"address": "0x9d9b602CFe69cfF9706EAc399808E84682ce94FB",
"owner": "0xabc123...",
"status": "ACTIVE",
"latency": "6.4ms",
"events": 0,
"lastHeartbeat": "2026-06-21T12:00:00.000Z",
"registeredAt": "2026-06-21T12:00:00.000Z"
}

Error: Already Registered (400)

{
"error": "Sentinel already registered for this address"
}

Error: Invalid Address (400)

{
"error": "Invalid contract address"
}

Implementation

export async function POST(request: Request) {
const body = await request.json();

if (typeof body?.address !== 'string' || !/^0x[a-fA-F0-9]{40}$/.test(body.address)) {
return NextResponse.json({ error: 'Invalid contract address' }, { status: 400 });
}

const node = await prisma.sentinelNode.create({
data: {
name: body.name || 'Custom Sentinel',
address: body.address,
owner: body.owner || null,
status: 'ACTIVE',
latency: '6.4ms',
events: 0
}
});

return NextResponse.json(node);
}

Usage

curl -X POST http://localhost:3000/api/sentinels \
-H "Content-Type: application/json" \
-d '{
"address": "0x9d9b602CFe69cfF9706EAc399808E84682ce94FB",
"name": "TargetVault",
"owner": "0xabc123..."
}'

PUT /api/sentinels

Toggles a sentinel node's status between ACTIVE and PAUSED.

Body

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
ParameterTypeRequiredDescription
idstringYesSentinel node ID

Response (200)

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "TargetVault",
"address": "0x9d9b602CFe69cfF9706EAc399808E84682ce94FB",
"status": "PAUSED",
...
}

Error: Not Found (404)

{
"error": "Sentinel node not found"
}

Implementation

export async function PUT(request: Request) {
const body = await request.json();
const existing = await prisma.sentinelNode.findUnique({ where: { id: body.id } });

if (!existing) {
return NextResponse.json({ error: 'Sentinel node not found' }, { status: 404 });
}

const updated = await prisma.sentinelNode.update({
where: { id: body.id },
data: {
status: existing.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE'
}
});

return NextResponse.json(updated);
}

Usage

curl -X PUT http://localhost:3000/api/sentinels \
-H "Content-Type: application/json" \
-d '{"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}'

Database Backend

The sentinels API uses the prisma adapter (frontend/src/lib/db.ts) which supports both PostgreSQL and in-memory storage:

PostgreSQL

When DATABASE_URL is set, data persists across restarts in the sentinel_nodes table:

CREATE TABLE IF NOT EXISTS sentinel_nodes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
address TEXT NOT NULL UNIQUE,
owner TEXT,
status TEXT NOT NULL CHECK (status IN ('ACTIVE', 'PAUSED', 'OFFLINE')),
latency TEXT NOT NULL DEFAULT '6.4ms',
events INTEGER NOT NULL DEFAULT 0,
last_heartbeat TIMESTAMPTZ NOT NULL DEFAULT NOW(),
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

In-Memory

When DATABASE_URL is not configured, data is stored in a global variable and lost on restart. A seed node is pre-populated:

const seedNodes: SentinelNode[] = [
{
id: 'sentinel-ax-node',
name: 'Sentinel.ax Node',
address: '0x9f758be3ae3D985713964339E2f0bD783fC6015c',
owner: null,
status: 'ACTIVE',
latency: '8ms',
events: 939,
lastHeartbeat: new Date(),
registeredAt: new Date('2026-01-03T00:00:00.000Z')
}
];

Agent Integration

The Python agent fetches the sentinel list on every scan iteration:

def get_registered_protocols():
url = frontend_api_url("sentinels")
req = urllib.request.Request(url, method='GET')
with urllib.request.urlopen(req, timeout=3) as res:
data = json.loads(res.read().decode('utf-8'))
db_addrs = [node['address'].lower() for node in data if 'address' in node]
return db_addrs + ["0x596Ff2Ca0f781a2CED29EC685cD1ba038378dE02".lower()]

Next Steps