> Every Sentinel doc, as Markdown: https://runsentinel.app/llms.txt

# SDK

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

```sh
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](/docs/api) routes.

{/* sdk-methods:start */}
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 |
{/* sdk-methods:end */}

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.
