Installation
npm install @atbash/sdkRequires Node.js 18 or higher. Server-side only — private keys are used for local signing and must never be exposed to browsers.
Quickstart
Load an agent, judge the pending action, then branch on ALLOW / HOLD / BLOCK before anything irreversible runs.
import { Atbash, loadAgent } from "@atbash/sdk";
// 1. Load your agent identity using the private key you saved during
// agent creation. loadAgent() validates the key and derives the
// matching public key for you.
const agent = loadAgent(process.env.ATBASH_AGENT_PRIVKEY!);
// 2. Build a client once and reuse it.
const client = new Atbash(agent.privkey);
// 3. Submit an action for judgment, before executing it
const result = await client.judgeAction(
"Transfer $50,000 to external wallet 0xabc",
"Outbound AML check — new recipient, over threshold",
);
// 4. Enforce the verdict
switch (result.verdict) {
case "ALLOW":
// Proceed with the action
break;
case "HOLD":
// Held — operator must approve in the dashboard
console.log("Held for review:", result.toolCallId);
break;
case "BLOCK":
// Refused — agent is jailed in Enforcement tier
throw new Error(`Blocked: ${result.reason}`);
}Before this works, the agent must be onboarded at atbash.ai — assigned to an org, with a policy pack attached, and the org tier set to Audit+ or Enforcement.
Don't have an agent yet?
There are two ways to create an agent:
1. Dashboard (recommended) — create an agent at atbash.ai/risk-engine/agents. The dashboard generates the keypair, assigns the agent to your org, and lets you attach a policy pack — all in one step.
2. Programmatic — generate a new keypair locally or bring an existing secp256k1 private key from another platform, then onboard it via the dashboard:
import { generateKeypair, loadAgent } from "@atbash/sdk";
const { priv_key } = generateKeypair();
// Persist priv_key to your secret manager (AWS Secrets Manager, Vault, etc.).
// Never log it, commit it, or print it to stdout — it controls the agent's identity.
const agent = loadAgent(priv_key);Note: After generating a key programmatically, you must still onboard the agent at atbash.ai/risk-engine/agents — assign it to an org and attach a policy pack before judgeAction will work.
Secret storage
The private key is used to sign on-chain transactions locally. Treat it like any other long-lived credential:
- Load it from an environment variable (
ATBASH_AGENT_PRIVKEY) or a secret manager (AWS Secrets Manager, HashiCorp Vault, 1Password, etc.) — never hardcode it. - Never commit
.envfiles containing the key. Add them to.gitignore. - If a key leaks, revoke the agent and create a new one in the Atbash dashboard.
- The SDK is server-side only — the key must never ship to a browser bundle.
Verdicts
Every judgeAction call returns one of three verdicts:
| Verdict | Meaning | What your code should do |
|---|---|---|
| ALLOW | Action is within policy | Proceed with execution |
| HOLD | Requires operator review | Pause — poll getJudgmentStatus until resolved |
| BLOCK | Violates a red line | Abort — agent is jailed in Enforcement tier |
NB: If your org is on the Audit tier, the judge returns "No verdict" — actions are logged on-chain for the audit trail but not evaluated by an AI provider. Upgrade to Audit+ or Enforcement at atbash.ai/risk-engine/settings for active verdicts.
API
Judge
Signature
new Atbash(privkey: string, options?: AtbashOptions)
client.judgeAction(
action: string,
context?: string,
options?: JudgeOptions,
): Promise<JudgeResult>Submit an action for judgment before execution. Returns the verdict, reason, confidence, provider, latency, and tool call ID.
Parameters
actionstringThe operation the agent is about to perform.
contextstringOperational context for the risk engine (blast radius, AML notes, etc.).
optionsJudgeOptions (optional)Inference provider, model, org name, audit fields. The endpoint and timeout are set on the client.
AgentAuth
interface AgentAuth {
pubkey: string; // 66-char hex, secp256k1 compressed public key
privkey: string; // 64-char hex private key
}JudgeOptions
interface JudgeOptions {
endpoint?: string; // API base URL (default: https://atbash.ai)
timeout?: number; // Request timeout in ms
provider?: string; // "openai" | "google" | "microsoft" | "custom"
model?: string; // Model override (any id your provider accepts)
toolName?: string; // Tool name for audit trail
toolArgsJson?: string; // Tool arguments JSON for audit trail
chainOpts?: ChainOpts; // Override Chromia chain connection
verifyPubKey?: string; // 66-hex pubkey for self-hosted judge response verification
}JudgeResult
interface JudgeResult {
verdict: string; // "ALLOW", "HOLD", "BLOCK", or "No verdict"
actionType: string; // "allow", "hold_for_user_confirm", "block", or a value added later
reason: string; // Human-readable explanation
confidence: number; // 0–1
provider: string; // Which provider evaluated the action
latencyMs: number; // Inference time
toolCallId: string; // Unique ID for this judgment
onChain: boolean; // Whether the record was written on-chain
enforced: boolean; // Whether the org's mode acts on HOLD/BLOCK
enforcementMode: string; // "off", "monitor", or "enforce"
status: string; // "logged" on the AUDIT tier, which returns no verdict
score?: number; // 1 (poisonous) to 10 (benign); memory scans only
allow: boolean; // Canonical "safe to execute"; gate on this
}Read status before verdict. On the AUDIT tier the judge logs the action and deliberately returns no verdict, and status: "logged" is the only thing that separates that from a verdict going missing. Do not infer it from an empty verdict.
The SDK trims and lower-cases actionType before handing it to you, so a judge answering ALLOW or " allow " arrives as allow. The field stays open-ended either way, so treat a value you do not recognise as non-permissive rather than switching only on the three known ones.
The simpler answer: gate on allow. It folds the verdict, the action type and the tier into one boolean and is never more permissive than the raw fields, so you cannot get it wrong by reading one signal and missing another.
Poll judgment status
Signature
client.getJudgmentStatus(judgmentId: string, agentPubkey?: string, chainOpts?: ChainOpts): Promise<JudgmentStatus>Check whether a held action has been approved or rejected by an operator.
chainOpts routes the poll to a specific chain. Pass it when the judgment was posted on a chain other than the client's default, such as a custom or self-hosted one, or the poll looks for the record on the wrong chain and returns a 404. A client already constructed on that chain does not need it.
interface ChainOpts {
network?: "private"; // the only value; the public chain is retired
blockchainRid?: string;
nodeUrls?: readonly string[];
}interface JudgmentStatus {
status: "pending" | "answered" | "error";
verdict: string;
reason: string;
judgmentId: string;
onChain?: boolean;
cached?: boolean;
responseTimeMs?: number;
}Agent identity
loadAgent(privkey: string): AgentAuth
generateKeypair(): { priv_key: string; pub_key: string }
derivePublicKey(privKeyHex: string): string
isValidPrivateKey(hex: string): boolean
pubkeyToHex(val: unknown): string
loadAgent(privkey) is the canonical loader — pass in the private key from the dashboard, get back { pubkey, privkey } ready for judgeAction. It accepts 0x-prefixed, padded, or mixed-case input and throws on malformed keys. Use generateKeypair() only for local development; for production, create agents in the dashboard so operators can attach policies.
Query APIs
Read from the Atbash audit trail and operator queues:
// Tool call history
getToolCalls(maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>
getOrgToolCalls(orgName: string, maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>
getAgentToolCalls(agentPubkey: string, maxCount: number, opts?: ClientOpts): Promise<ToolCallRecord[]>
getToolCallCount(opts?: ClientOpts): Promise<number>
getToolCallFull(toolCallId: string, opts?: ClientOpts): Promise<ToolCallFull | null>
// Org and agent info
getOrgTierInfo(orgName: string, opts?: ClientOpts): Promise<TierInfo | null>
getAgentDetail(agentPubkey: string, opts?: ClientOpts): Promise<Record<string, unknown>>
getAgentPolicy(agentPubkey: string, opts?: ClientOpts): Promise<{ policy: string; is_jailed: boolean; is_custom: boolean; default_policy: string }>
// Operator review queue
getPendingHeldActions(orgName: string, maxCount: number, opts?: ClientOpts): Promise<HeldAction[]>
getHeldActionReviews(orgName: string, maxCount: number, opts?: ClientOpts): Promise<HeldActionReview[]>
// Chain-wide stats
getSafetyStats(opts?: ClientOpts): Promise<Record<string, unknown>>All query functions accept an optional ClientOpts to override the endpoint:
interface ClientOpts {
endpoint?: string; // Default: https://atbash.ai
timeout?: number;
}Configuration
new Atbash(privkey, options) takes everything explicitly. Atbash.fromConfig(options) fills in each setting from the first place that has it: the option, then the environment variable, then ~/.config/atbash/config.json. The signing key is the exception: it is never read from config.json. judge is taken whole: pass it complete, or leave it out and use the environment. A judge with only an endpoint ignores ATBASH_JUDGE_VERIFY_PUBKEY, so a self-hosted host then fails the allowlist.
- Key. The
agentKeyoption, thenkeyPath, thenATBASH_AGENT_KEY, then~/.config/atbash/guard-client-key, then~/.config/atbash/atbash-client-key. A key file holdsprivkey=/pubkey=lines or JSON withprivKey/pubKey. AnagentKeyleft inconfig.jsonis ignored with a warning. - Endpoint.
judge.endpoint, thenATBASH_ENDPOINT, thenjudgeEndpointin the file. Onlyhttps://atbash.aiandwww.atbash.ai(and the host a dev build was compiled for) are accepted as is; any otherhttps://judge needspolicy: "self-hosted"plus its 66-hexverifyPubKey(orATBASH_JUDGE_VERIFY_PUBKEY). Plainhttp://is accepted only for localhost. - Org.
orgNamepicks the chain the call is signed for. Which org's policy judges the action comes from the agent's registration, not from this string, and a wrong name is not an error.
The full rules, with the exact error messages and a key-file-to-first-ALLOW walkthrough, are in the SDK README's Configuration resolution section.
// Self-hosted judge, set on the client rather than per call. A host outside
// the allowlist is accepted only with the judge's response-signing key.
const client = new Atbash(agent.privkey, {
endpoint: "https://your-instance.example.com",
verifyPubKey: "02…66 hex…",
});
// Custom provider (API keys are saved in the dashboard, not passed here)
const result = await client.judgeAction(action, context, {
provider: "openai",
model: "<model-id>",
});Integration patterns
Drop-in shapes for gating execution, waiting on operator approval, and checking jail state before batch work.
Pre-execution gate
Judge first; only run the side effect when verdict is ALLOW.
async function safeExecute(action: string, context: string, execute: () => Promise<void>) {
const result = await client.judgeAction(action, context);
if (result.verdict === "BLOCK") throw new Error(`Blocked: ${result.reason}`);
if (result.verdict === "HOLD") throw new Error(`Held for review: ${result.toolCallId}`);
await execute();
}Polling a held action
Loop until the operator resolves the judgment in the dashboard.
async function waitForApproval(toolCallId: string, agentPubkey: string): Promise<string> {
while (true) {
const status = await getJudgmentStatus(toolCallId, agentPubkey);
if (status.status === "answered") return status.verdict;
if (status.status === "error") throw new Error(status.reason);
await new Promise((r) => setTimeout(r, 5000));
}
}Checking agent jail status before a batch
Fail fast if Enforcement tier has jailed the agent.
const policy = await getAgentPolicy(pubKey);
if (policy.is_jailed) {
console.error("Agent is jailed — unjail via dashboard before retrying.");
process.exit(1);
}Error handling
The SDK throws standard Error objects. Known failure modes are enriched with a pointer to the relevant dashboard page, so the error message tells you where to fix the problem:
API error 404: {"error":"Agent not registered..."}
→ Onboard the agent at https://atbash.ai/risk-engine/agents| Error | Cause | Where to fix |
|---|---|---|
| API error 404: Agent not registered | Agent not onboarded | atbash.ai/risk-engine/agents |
| API error 400: Agent has no policy | No policy attached to agent | atbash.ai/risk-engine/agents |
| Agent is jailed | BLOCK verdict triggered auto-jail (Enforcement tier) | atbash.ai/risk-engine/agents |
| Org tier does not support verdicts | Org is on Audit tier | atbash.ai/risk-engine/settings |
| API error 400: action is required | Empty action string | Fix caller |
| API error 502: Incorrect API key | Invalid provider API key | atbash.ai/risk-engine/settings |
try {
const result = await client.judgeAction(action, context);
} catch (err) {
if (err instanceof Error && err.message.includes("Agent not registered")) {
// Point the user at https://atbash.ai/risk-engine/agents to onboard.
}
}Dashboard
Policy authoring, operator reviews, and agent management happen at atbash.ai. The SDK is the programmatic interface; the dashboard is the operator interface.
License
Apache License 2.0. See LICENSE.