sentinelHow it worksPricingBlogSign in

Start

IntroductionQuickstartAuthentication

Guides

Monitor a contractAdmin changesLarge outflowssBTC and PoX-5Slack and webhooks

Concepts

Watches & alertsBilling

Reference

CLIMCP serverSDKAPI reference
Powered bysecondlayer
Reference / SDK
Previous← MCP serverNextAPI reference →

On this page

SDK

The TypeScript client for Stacks contract monitoring (@secondlayer/sentinel): watches, alerts and signed webhook checks. Node 22 or newer.

shell
npm i @secondlayer/sentinel

Create a client

createClient takes a token. resolveToken finds one the way the CLI does: SENTINEL_API_KEY, a saved key, then the saved login. It returns null when there is none.

ts
import { createClient, resolveToken } from "@secondlayer/sentinel";

const credential = await resolveToken();
if (!credential) throw new Error("Not logged in. Run: sentinel login");
const sentinel = createClient({ token: credential.token });

In CI, pass the key from the environment:

ts
const sentinel = createClient({ token: process.env.SENTINEL_API_KEY! });

baseUrl is optional and defaults to https://api.runsentinel.app.

The grammar's verbs

The client has the CLI's verbs as methods, named from them (destinations add → destinationsAdd): when, every, at, describe, list, show, logs, on, off, read, rename, remove, group, whoami, import, backtest. Each takes a target as the CLI does (a contract's id or its name on your account, or an id) and answers the CLI's shape: a write { done, willDo, effect, next }; a read its data and next. A free write happens at once and dryRun: true previews it; a cost (a check, a contract over your tier's count) or stopping coverage answers done: false until yes: true.

ts
const preview = await sentinel.when("transfer example-vault sBTC --out --over 5", { dryRun: true });
preview.effect;   // the 90-day backtest and the cost
const added = await sentinel.when("transfer example-vault sBTC --out --over 5", { wait: true });
const { entries, nextCursor } = await sentinel.logs("example-vault");

when also takes a TriggerSpec. parseLine, specLine and resolveTarget are exported too. The verbs whose name a method below already has (contracts, alerts, testAlert, destinations, ask, redeliver, retryHistory, update, autoPlan, link, planLinked, connectSlack) keep that method in this release; their verb is verbsOf(sentinel).<name>.

List and read plans

ts
const plans = await sentinel.plans();        // your contracts
const plan = await sentinel.plan(plans[0].planId);

Turn a watch on

The API methods below go straight through: they have no preview step. Read first, then call. The verbs above write at once unless they cost money or stop coverage.

ts
const { watches } = await sentinel.list();
await sentinel.on(watches[0].id);                      // a watch is a w_… id; live once its contract's monitoring is on
await sentinel.on("example-vault");                    // a contract: resumes its monitoring, counts toward your tier once live

Every write answers monitoring, the contract's status: only live (or requested, setting up) means it is watched. Where alerts go is your account's destinations (destinations), not a setting on a contract.

createPlan starts a plan. Plans are free, and nothing is turned on by one. previewPlan writes nothing:

ts
const preview = await sentinel.previewPlan("SP000000000000000000002Q6VF78.example-vault");
const { planId } = await sentinel.createPlan(preview.contractId);

show takes a contract too: it starts the plan and reads back what its code has: its functions, its tokens, the @admin, @value and @control functions and the events it prints. It turns nothing on.

ts
const r = await sentinel.show("SP000000000000000000002Q6VF78.example-vault");
if (r.kind === "contract") r.contract.groups.control;   // the functions that change who controls the contract

A whole Clarinet repo goes the same way: previewImport lists its contracts at a pinned commit, and importRepo plans the ones you pick at exactly that commit and adds them to your account (hold: true keeps their watches off after the deploy):

ts
const repo = await sentinel.previewImport("https://github.com/acme/protocol");
const picked = repo.contracts.filter((c) => c.suggested).map((c) => c.name);
const { plans } = await sentinel.importRepo({
  url: "https://github.com/acme/protocol",
  commit: repo.commit,
  contracts: picked,
});

Errors

A failed call throws SentinelError with the HTTP status and the API's message.

ts
import { SentinelError } from "@secondlayer/sentinel";

try {
  await sentinel.on("w_example");
} catch (e) {
  if (e instanceof SentinelError && e.status === 403) console.error(e.message);
  else throw e;
}

Every method

Each capability has a method, named from its verb, or (this release) the API method its route had. Methods map to the API routes.

The verbs: when, every, at, describe, list, show, logs, on, off, read, rename, remove, group, whoami, upgrade, checkoutStatus, statusWatch, import, backtest, destinationsAdd, destinationsDefault, destinationsRemove. Each previews (done: false) until yes: true and answers next.

MethodRouteNeeds
addDestinationPOST /v1/destinationsMonitoring: write
addWatchPOST /v1/watchesMonitoring: write
alertGET /v1/alerts/:idAlerts: read
alertPreviewGET /plan/:id/alert-previewMonitoring: read
alertsGET /v1/alertsAlerts: read
askPOST /askany key
autoPlanPOST /plan/:id/waitPlans: write
backtestCheckPOST /v1/checks/backtestMonitoring: write
billingGET /billingany key
checkRunGET /v1/checks/:idMonitoring: read
checkRunsGET /v1/checksMonitoring: read
compileWatchPOST /v1/watches/compileMonitoring: write
connectSlackPOST /integrations/slack/installMonitoring: write
contractsGET /v1/contractsPlans: read
createKeyPOST /keysyour own sign-in
createPlanPOST /planPlans: write
createSchedulePOST /v1/schedulesMonitoring: write
deleteScheduleDELETE /v1/schedules/:idMonitoring: write
destinationsGET /v1/destinationsMonitoring: read
disconnectSlackDELETE /integrations/slack/:teamMonitoring: write
groupsGET /v1/groupsMonitoring: read
importRepoPOST /contracts/importPlans: write
keysGET /keysyour own sign-in
latestGET /plan/:id/latestPlans: read
linkPOST /plan/:id/linkPlans: write
logEntriesGET /v1/logsMonitoring: read
meGET /v1/meany key
muteAlertPOST /alerts/:id/muteAlerts: write
planGET /v1/plans/:idPlans: read
planLinkedPOST /plan/:id/plan-linkedPlans: write
plansGET /v1/plansPlans: read
previewImportPOST /contracts/import/previewPlans: read
previewPlanPOST /plan/previewany key
previewSchedulePOST /v1/schedules/previewMonitoring: read
previewWatchPOST /v1/watches/previewMonitoring: read
publicWatchGET /v1/watches/:idMonitoring: read
publicWatchesGET /v1/watchesMonitoring: read
readAlertPOST /alerts/:id/readAlerts: write
readAllAlertsPOST /alerts/read-allAlerts: write
redeliverPOST /alerts/:id/redeliverAlerts: write
removeContractPOST /plan/:id/removePlans: write
removeDestinationDELETE /v1/destinations/:idMonitoring: write
renameContractPOST /plan/:id/settingsPlans: write
resumeSchedulePOST /v1/schedules/:id/onMonitoring: write
retryHistoryPOST /plan/:id/history/retryMonitoring: write
revokeKeyPOST /keys/:id/revokeyour own sign-in
scheduleGET /v1/schedules/:idMonitoring: read
schedulesGET /v1/schedulesMonitoring: read
setDefaultDestinationPOST /v1/destinations/:id/defaultMonitoring: write
setGroupPOST /v1/groupsMonitoring: write
setWatchDeliveryPOST /v1/watches/:id/deliveryMonitoring: write
setWatchOnPOST /v1/watches/:id/stateMonitoring: write
slackInstallsGET /integrations/slackMonitoring: read
startMonitoringPOST /plan/:id/monitorMonitoring: write
statusGET /v1/statusany key
stopMonitoringDELETE /plan/:id/monitorMonitoring: write
testAlertPOST /plan/:id/testalertMonitoring: write
updatePOST /plan/:id/updatePlans: write

A remove takes the name the app asks you to type:

ts
const contracts = await sentinel.contracts();
const vault = contracts.find((c) => c.name === "example-vault")!;
await sentinel.removeContract(vault.planId, vault.name);

Sign-in and choosing a tier stay on the account page (/account); keys need your own login.