Skip to content
BLACKLAKE
SDK govern()▾ docs nav

SDK governance — five-minute quick start

Use the BlackLake SDK when your code calls LLMs or tools directly — backend services, custom AI Actors, batch jobs, anything the MCP gateway doesn't see on the wire. The SDK gives you bl.govern() for the policy decision and bl.cost.record() for self-attributed cost.

This is the right path when you've got code already; the CLI tools quick start is the right path when you've got a Claude Code / Codex / Cursor terminal.

Install#

npm install blacklake

1. Configure the client#

import { BlackLake, suggestActorName, detectSession } from 'blacklake';

const bl = new BlackLake({
  apiKey: process.env.BLACKLAKE_API_KEY,    // bl_… (workspace) or bl_usr_… (per-user)
  baseUrl: process.env.BLACKLAKE_BASE_URL ?? 'https://api.blacklake.systems',
});

Use a workspace key for backend services (no per-user attribution needed) or a user-scoped key when calls represent a specific human.

2. Register the agent + tool + binding#

You only do this once per workspace — the records persist. Either through the console (/agents, /tools) or via the SDK:

const agent = await bl.agents.create({
  name: 'expense-bot',
  environment: 'production',
  risk_classification: 'high',
});

const tool = await bl.tools.create({
  name: 'stripe.refund',
  risk_classification: 'critical',
});

await bl.agents.bindTool(agent.id, tool.id);

The binding matters. Without it every govern() call denies with reason "Tool is not bound to agent" — see Policy guide → Bind your tools first.

3. Wrap every consequential action with govern()#

const session = detectSession({ tool_client: 'expense-bot', tool_client_version: '1.4.2' });

const decision = await bl.govern({
  agent: 'expense-bot',
  tool: 'stripe.refund',
  action: { customer_id: 'cus_xyz', amount_cents: 4200 },
  context: { session },
  estimate: {
    provider: 'anthropic',
    model: 'claude-opus-4-7',
    input_tokens: 1200,
    output_ceiling_tokens: 800,
    estimated_cost_usd: 0.12,
  },
});

if (decision.decision === 'allow') {
  await stripe.refunds.create({ customer: 'cus_xyz', amount: 4200 });
}

What you get back:

  • decision.decisionallow / deny / approval_required / default_deny. Branch on this.
  • decision.decision_token — HMAC-signed receipt. Quote it when reporting outcomes; bl.decisions.verify(token) proves the decision was real.
  • decision.budget_headroom_usd — how close the next call would be to denying. Surface in your operator UI.
  • decision.evaluation_id — opens at console.blacklake.systems/evaluations/<id>.

4. Record cost the gateway didn't see#

When your code calls the LLM directly (Bedrock / Vertex / your own router), the gateway never saw the wire — feed cost in:

await bl.cost.record({
  evaluation_id: decision.evaluation_id,
  provider: 'anthropic',
  model: 'claude-opus-4-7',
  input_tokens: 1200,
  output_tokens: 850,
  cache_read_tokens: 4000,
  cache_write_tokens: 0,
  thinking_tokens: 0,
});

The receipt upgrades to v2 (cost-bound) once cost lands, and the Usage page picks it up immediately.

5. Verify a decision later#

A receipt is only worth something if it can be verified independently:

const result = await bl.decisions.verify({
  evaluation_id: decision.evaluation_id,
  decision_token: decision.decision_token,
});
if (result.valid) {
  // result.decision, result.policy_name, result.policy_snapshot, result.action_results
}

Pass this through to anyone receiving an "approved" or "denied" claim from your service — they can verify it without trusting your code.

What's next#