The TypeScript client for Stacks contract monitoring (@secondlayer/sentinel): watches, alerts and signed webhook checks. Node 22 or newer.
npm i @secondlayer/sentinelcreateClient 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.
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:
const sentinel = createClient({ token: process.env.SENTINEL_API_KEY! });baseUrl is optional and defaults to https://api.runsentinel.app.
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.
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>.
const plans = await sentinel.plans(); // your contracts
const plan = await sentinel.plan(plans[0].planId);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.
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 liveEvery 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:
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.
const r = await sentinel.show("SP000000000000000000002Q6VF78.example-vault");
if (r.kind === "contract") r.contract.groups.control; // the functions that change who controls the contractA 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):
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,
});A failed call throws SentinelError with the HTTP status and the API's message.
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;
}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.
| Method | Route | Needs |
|---|---|---|
addDestination | POST /v1/destinations | Monitoring: write |
addWatch | POST /v1/watches | Monitoring: write |
alert | GET /v1/alerts/:id | Alerts: read |
alertPreview | GET /plan/:id/alert-preview | Monitoring: read |
alerts | GET /v1/alerts | Alerts: read |
ask | POST /ask | any key |
autoPlan | POST /plan/:id/wait | Plans: write |
backtestCheck | POST /v1/checks/backtest | Monitoring: write |
billing | GET /billing | any key |
checkRun | GET /v1/checks/:id | Monitoring: read |
checkRuns | GET /v1/checks | Monitoring: read |
compileWatch | POST /v1/watches/compile | Monitoring: write |
connectSlack | POST /integrations/slack/install | Monitoring: write |
contracts | GET /v1/contracts | Plans: read |
createKey | POST /keys | your own sign-in |
createPlan | POST /plan | Plans: write |
createSchedule | POST /v1/schedules | Monitoring: write |
deleteSchedule | DELETE /v1/schedules/:id | Monitoring: write |
destinations | GET /v1/destinations | Monitoring: read |
disconnectSlack | DELETE /integrations/slack/:team | Monitoring: write |
groups | GET /v1/groups | Monitoring: read |
importRepo | POST /contracts/import | Plans: write |
keys | GET /keys | your own sign-in |
latest | GET /plan/:id/latest | Plans: read |
link | POST /plan/:id/link | Plans: write |
logEntries | GET /v1/logs | Monitoring: read |
me | GET /v1/me | any key |
muteAlert | POST /alerts/:id/mute | Alerts: write |
plan | GET /v1/plans/:id | Plans: read |
planLinked | POST /plan/:id/plan-linked | Plans: write |
plans | GET /v1/plans | Plans: read |
previewImport | POST /contracts/import/preview | Plans: read |
previewPlan | POST /plan/preview | any key |
previewSchedule | POST /v1/schedules/preview | Monitoring: read |
previewWatch | POST /v1/watches/preview | Monitoring: read |
publicWatch | GET /v1/watches/:id | Monitoring: read |
publicWatches | GET /v1/watches | Monitoring: read |
readAlert | POST /alerts/:id/read | Alerts: write |
readAllAlerts | POST /alerts/read-all | Alerts: write |
redeliver | POST /alerts/:id/redeliver | Alerts: write |
removeContract | POST /plan/:id/remove | Plans: write |
removeDestination | DELETE /v1/destinations/:id | Monitoring: write |
renameContract | POST /plan/:id/settings | Plans: write |
resumeSchedule | POST /v1/schedules/:id/on | Monitoring: write |
retryHistory | POST /plan/:id/history/retry | Monitoring: write |
revokeKey | POST /keys/:id/revoke | your own sign-in |
schedule | GET /v1/schedules/:id | Monitoring: read |
schedules | GET /v1/schedules | Monitoring: read |
setDefaultDestination | POST /v1/destinations/:id/default | Monitoring: write |
setGroup | POST /v1/groups | Monitoring: write |
setWatchDelivery | POST /v1/watches/:id/delivery | Monitoring: write |
setWatchOn | POST /v1/watches/:id/state | Monitoring: write |
slackInstalls | GET /integrations/slack | Monitoring: read |
startMonitoring | POST /plan/:id/monitor | Monitoring: write |
status | GET /v1/status | any key |
stopMonitoring | DELETE /plan/:id/monitor | Monitoring: write |
testAlert | POST /plan/:id/testalert | Monitoring: write |
update | POST /plan/:id/update | Plans: write |
A remove takes the name the app asks you to type:
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.