Documentation

Record what your AI agent is allowed to do. One line of code.

Quickstart

Get up and running in under 2 minutes.

1. Install the SDK

pip install agentcheck-sdk

2. Get your API key

from agentcheck import Client

result = Client.signup(
    email="[email protected]",
    company_name="Your Company",
    base_url="https://agentcheck.spaceplanning.work"
)
print(result["api_key"])  # Save this! Shown only once.
Your API key starts with ak_live_. Store it securely. It cannot be retrieved after signup.

3. Create your first agreement

import agentcheck

agentcheck.init(
    api_key="ak_live_your_key_here",
    base_url="https://agentcheck.spaceplanning.work"
)

proof = agentcheck.record(
    agent="my-bot",
    scope="read database, send reports",
    authorized_by="[email protected]"
)

print(proof.id)         # Agreement ID
print(proof.status)     # "pending" until approved
print(proof.verify_url) # Public verification link

Approval Flow

When you create an agreement, the authorized_by email receives a message with an "Approve" button. No account creation needed - one click approves.

After approval:

record = agentcheck.get(proof.id)
print(record.status)      # "approved"
print(record.approved_at) # timestamp

Public Verification

Every agreement has a public verification page that anyone can access:

https://agentcheck.spaceplanning.work/api/v1/verify/{agreement_id}

The page shows the agreement details, approval status, HMAC signature validity, and a QR code for physical display.

Amendments

Need to change the scope? Amend the agreement:

amended = agentcheck.amend(
    agreement_id=proof.id,
    new_scope="read database, send reports, deploy to staging",
    reason="Added staging deployment permission"
)
# Sends re-approval email to the authorizer

Webhooks

Get notified in real-time when agreements are approved or amended:

client = agentcheck.Client(
    api_key="ak_live_...",
    base_url="https://agentcheck.spaceplanning.work"
)

webhook = client.register_webhook(
    url="https://your-server.com/webhook",
    events=["record.approved", "record.amended"]
)
print(webhook["secret"])  # whsec_... for HMAC verification

Verifying webhook signatures

from agentcheck.webhook import WebhookHandler

handler = WebhookHandler(secret="whsec_...")

# In your webhook endpoint:
event = handler.verify_and_parse(
    body=request.body,
    signature=request.headers["X-AgentCheck-Signature"]
)
print(event.type) # "record.approved"
print(event.data) # agreement details

Agreement Lifecycle

Every agreement follows this state machine:

                     email click
  pending ──────────────────────────→ approved
     │                                      │
     │ expires_at passed                    │ POST /revoke
     ↓                                      ↓
  expired                               revoked
                                            │
     ↑                                      │
     │ expires_at passed                    │
     │                                      │
  approved ────────────────────────────┘

  POST /amend (on approved) → back to pending (re-approval needed)
Immutability: Once an agreement is approved or revoked, its core fields (agent, scope, authorized_by, signature) cannot be modified. Only status transitions are allowed. This is enforced at the database level with triggers.

Status descriptions

StatusMeaningTransitions to
pendingCreated, waiting for email approvalapproved, expired
approvedAuthorizer clicked "Approve" in emailrevoked, expired, pending (via amend)
revokedExplicitly revoked by API key ownerNone (terminal)
expiredPast expires_at date (auto-detected every 5 min)None (terminal)

Revoking Agreements

Immediately revoke an agreement when an agent should no longer have authorization:

# Python
agentcheck.revoke(
    agreement_id="abc123",
    reason="Security incident - agent compromised"
)

// TypeScript / Node.js
await fetch(`${BASE_URL}/api/v1/record/${id}/revoke`, {
  method: 'POST',
  headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
  body: JSON.stringify({ reason: 'Security incident' })
})
Revocation is immediate and irreversible. The agreement status changes to revoked and a record.revoked webhook is fired.

Integration Guide

How to integrate AgentCheck into your system. This section covers what you need to build on your side.

What AgentCheck does vs. what you do

ResponsibilityAgentCheckYour System
Create delegation recordAPI providedCall our API
Tamper-proof signatureAutomatic (HMAC-SHA256)Nothing to do
Send approval emailAutomatic (SendGrid)Nothing to do
Store records immutablyPostgreSQL + DB triggersNothing to do
Public verification page/verify/:id with QRNothing to do
Expiry detectionAuto-scan every 5 minNothing to do
Adapter (API field mapping)-You build this
Action verification (is this in scope?)-You build this
Approve flow UXEmail linkHandle async approval
Webhook receiverSends eventsYou receive and process

Building an Adapter

Your system likely has its own delegation/authorization model. You need an adapter - a thin layer that translates between your model and the AgentCheck API.

// TypeScript example: Adapter pattern

interface YourDelegationProvider {
  create(agentId: string, scope: object, delegator: string): Promise<Delegation>
  revoke(id: string, reason: string): Promise<void>
  verify(id: string): Promise<boolean>
}

// Your adapter that wraps AgentCheck
class AgentCheckAdapter implements YourDelegationProvider {
  private baseUrl: string;
  private apiKey: string;

  async create(agentId: string, scope: object, delegator: string) {
    // Map YOUR fields to AgentCheck fields
    const resp = await fetch(`${this.baseUrl}/api/v1/record`, {
      method: 'POST',
      headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        agent: agentId,                        // your agentId -> our agent
        scope: JSON.stringify(scope),           // your object -> our string
        authorized_by: delegator,              // your delegator -> our authorized_by
      })
    });
    return resp.json();
  }

  async revoke(id: string, reason: string) {
    await fetch(`${this.baseUrl}/api/v1/record/${id}/revoke`, {
      method: 'POST',
      headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({ reason })
    });
  }

  async verify(id: string) {
    const resp = await fetch(`${this.baseUrl}/api/v1/record/${id}`, {
      headers: { 'X-API-Key': this.apiKey }
    });
    const data = await resp.json();
    return data.data.status === 'approved';
  }
}

Understanding the Approval Flow

AgentCheck uses email-based approval, which is fundamentally different from synchronous API approval:

Synchronous (your local system)AgentCheck (email approval)
FlowPOST /approve -> instantEmail sent -> human clicks -> webhook fires
SpeedMillisecondsMinutes to hours
EvidenceInternal log onlyEmail receipt + IP + timestamp + signature
Legal weightWeak (self-signed)Strong (third-party record + human action)

How to handle async approval in your system:

// 1. Create agreement (status = pending)
const proof = await adapter.create(agent, scope, email);
// Agent should NOT act yet

// 2. Register webhook to get notified
// POST /api/v1/webhooks { url: "https://your-server/webhook", events: ["record.approved"] }

// 3. In your webhook handler:
app.post('/webhook', (req, res) => {
  if (req.body.type === 'record.approved') {
    // NOW the agent can act
    enableAgent(req.body.data.agreement_id);
  }
});

// 4. Or poll periodically:
const record = await adapter.verify(proof.id);
if (record) { enableAgent(proof.id); }

Action Verification (Your Responsibility)

AgentCheck verifies that a delegation exists and is authentic. It does NOT verify whether a specific action falls within the scope. That logic depends on your business rules.

// This is YOUR code, not AgentCheck's
function isActionInScope(agreement, action) {
  // Parse the scope (you define the format)
  const scope = JSON.parse(agreement.scope);

  // Your business logic
  if (action.type === 'purchase' && action.amount > scope.maxAmount) {
    return false; // Over limit
  }
  return true;
}

// Full verification flow in your system:
async function canAgentAct(agentId, action) {
  // 1. Get delegation from AgentCheck (is it valid + authentic?)
  const records = await fetch(`${BASE_URL}/api/v1/records?agent=${agentId}&status=approved`);
  const delegation = records.data.records[0];
  if (!delegation) return false;  // No valid delegation

  // 2. Check if action is in scope (YOUR logic)
  return isActionInScope(delegation, action);
}
Why we don't do this: Scope interpretation is business-specific. "Order parts under $10K" means different things in different systems. If we judged scope for you, you'd have to trust our judgment - which defeats the purpose of a trust system. You read the scope, you decide.

Integration Scenarios

Scenario 1: AI Agent Framework (LangChain, CrewAI, AutoGen)

# Before your agent executes any tool, check delegation
import agentcheck

def execute_tool(agent_id, tool_name, params):
    # Check if agent has valid delegation
    records = agentcheck.list(agent=agent_id, status="approved")
    if not records.records:
        raise PermissionError(f"No valid delegation for {agent_id}")

    # Your scope check
    delegation = records.records[0]
    if not is_in_scope(delegation.scope, tool_name, params):
        raise PermissionError(f"Action {tool_name} not in scope")

    # Proceed with tool execution
    return run_tool(tool_name, params)

Scenario 2: ERP / Business System

// AI agent in your ERP wants to create a purchase order
async function createPurchaseOrder(agentId, order) {
  // 1. Check delegation exists and is approved
  const resp = await fetch(
    `${BASE_URL}/api/v1/records?agent=${agentId}&status=approved`,
    { headers: { 'X-API-Key': API_KEY } }
  );
  const delegations = (await resp.json()).data.records;
  if (!delegations.length) throw new Error('No delegation');

  // 2. Your scope check: is this order within limits?
  const scope = delegations[0].scope;
  if (order.amount > parseMaxAmount(scope)) {
    throw new Error(`Order $${order.amount} exceeds delegation limit`);
  }

  // 3. Create the order (agent is authorized)
  return db.purchaseOrders.create(order);
}

Scenario 3: DevOps / CI-CD Pipeline

# In your deployment script
import agentcheck

agentcheck.init(api_key=os.environ["AGENTCHECK_API_KEY"])

def deploy(bot_name, environment):
    records = agentcheck.list(agent=bot_name, status="approved")
    if not records.records:
        print(f"BLOCKED: {bot_name} has no delegation")
        sys.exit(1)

    scope = records.records[0].scope
    if environment == "production" and "production" not in scope:
        print(f"BLOCKED: {bot_name} not authorized for production")
        sys.exit(1)

    # Delegation verified - proceed with deployment
    run_deployment(environment)

Scenario 4: Customer Support Bot

// Support bot handling refunds
async function handleRefund(botId, customerId, amount) {
  // Check if bot has approved delegation
  const records = await fetch(
    `${BASE_URL}/api/v1/records?agent=${botId}&status=approved`,
    { headers: { 'X-API-Key': API_KEY } }
  );
  const delegation = (await records.json()).data.records[0];

  if (!delegation) {
    return { error: 'Bot not authorized. Contact admin.' };
  }

  // Your limit check
  if (amount > 500) {
    return { error: 'Amount exceeds bot refund limit. Escalating to human.' };
  }

  // Process refund with audit trail
  return processRefund(customerId, amount, delegation.id);
  // Anyone can verify: /verify/{delegation.id}
}

Scenario 5: Multi-Agent System

# Multiple agents, each with different scopes
import agentcheck

agentcheck.init(api_key="ak_live_...")

# Register delegations for each agent
monitor = agentcheck.record(
    agent="monitor-bot",
    scope="read server metrics, send alerts",
    authorized_by="[email protected]"
)

deployer = agentcheck.record(
    agent="deploy-bot",
    scope="deploy to staging only, rollback on failure",
    authorized_by="[email protected]"
)

orderer = agentcheck.record(
    agent="procurement-bot",
    scope="order supplies under $5K, approved vendors only",
    authorized_by="[email protected]"
)

# Each agent's delegation is independently approved via email
# Each has a public verification page with QR code
# Each can be independently revoked if compromised

Audit Trail (Hash Chain)

Every agreement event is recorded in a tamper-proof hash chain. Each entry links to the previous one cryptographically.

모든 위임장 이벤트가 변조 방지 해시 체인에 기록됩니다. 각 항목이 이전 항목에 암호학적으로 연결됩니다.

How it works / 작동 원리

Entry #1: hash = SHA256("genesis" + data1), sig = HMAC(hash1)
Entry #2: hash = SHA256(hash1 + data2),   sig = HMAC(hash2)
Entry #3: hash = SHA256(hash2 + data3),   sig = HMAC(hash3)

If any entry is tampered with, all subsequent hashes break.
하나라도 변조하면 이후 모든 해시가 깨집니다.

Three layers of protection / 3중 보호

LayerWhat it doesWhat it proves
Hash ChainEach entry contains SHA-256 of previous entryNo entries inserted, deleted, or reordered
DB TriggersUPDATE/DELETE physically blockedCannot modify after storage
HMAC SignatureServer signs each entryEntry was created by AgentCheck server

Recorded events / 기록되는 이벤트

EventWhen
record.createdAgreement created
record.approvedEmail approval clicked
record.revokedAgreement revoked
record.amendedScope changed
record.expiredAuto-expired by server

Get audit trail / 감사 추적 조회

# Python
trail = client.get_audit_trail("agreement-id")
for entry in trail["entries"]:
    print(f"{entry['event_type']} | hash: {entry['entry_hash'][:16]}...")

// TypeScript
const trail = await client.getAuditTrail("agreement-id");
trail.entries.forEach(e => console.log(e.event_type, e.entry_hash));

Verify chain integrity / 체인 무결성 검증

# Python - customer can verify independently
result = client.verify_audit_chain("agreement-id")
print(result["valid"])         # True = chain intact
print(result["total_entries"])  # number of entries

// TypeScript
const result = await client.verifyAuditChain("agreement-id");
console.log(result.valid);  // true
Why this matters: When a regulator asks "prove this agent was authorized on March 15th", you provide the audit trail. The hash chain proves no entries were added or removed after the fact. The HMAC proves our server created each entry. The DB triggers prove we couldn't have modified them even if we wanted to.

왜 중요한가: 규제기관이 "3월 15일에 이 에이전트가 승인됐다는 것을 증명하라"고 하면, 감사 추적을 제공합니다. 해시 체인은 사후에 항목이 추가/삭제되지 않았음을 증명합니다.

Safety Stack (Multi-Layer Verification)

Combine multiple verification layers for defense-in-depth. Each layer catches what the previous one misses.

다중 레이어 검증을 조합하여 심층 방어합니다. 각 레이어가 이전 레이어가 놓친 것을 잡습니다.

LayerWhat it catchesExample
ScopeEngineUnauthorized actions, amount limits$15K order when limit is $10K
SemanticVerifierIntent mismatch (LLM)Ordering luxury items as "maintenance parts"
BudgetTrackerCumulative overspend$9K x 6 times = $54K exceeds $50K daily
PatternMonitorAnomalous behavior50 actions today vs 3/day average
HumanEscalationHigh-risk decisions$8K+ orders require manager approval
# Python
from agentcheck import SafetyStack, BudgetTracker, PatternMonitor, HumanEscalation, build_scope

scope = build_scope(
    allowed=["order_parts", "monitor"],
    denied=["shutdown"],
    limits={"order_parts": {"max_amount": 10000}},
)

stack = SafetyStack(
    budget=BudgetTracker(daily_limit=50000),
    pattern=PatternMonitor(),
    escalation=HumanEscalation(threshold_amount=8000),
)

result = stack.check(scope, "order_parts", amount=3000)
# result.allowed = True, result.passed_layers = ["scope_engine", "budget_tracker", ...]

VerificationPipeline (Complete Flow)

Connects AgentCheck server + safety layers + LLM into one verify-and-execute call.

AgentCheck 서버 + 안전 레이어 + LLM을 하나의 검증-실행 호출로 연결합니다.

# Python
from agentcheck import Client, VerificationPipeline, ClaudeProvider, BudgetTracker, HumanEscalation

client = Client(api_key="ak_live_...", base_url="https://agentcheck.spaceplanning.work")
pipeline = VerificationPipeline(
    client=client,
    llm=ClaudeProvider(),                              # LLM semantic check (optional)
    budget=BudgetTracker(daily_limit=50000),
    escalation=HumanEscalation(threshold_amount=5000),
)

result = pipeline.verify_and_execute(
    agent="factory-bot",
    action="order_parts",
    amount=3000,
    execute_fn=lambda: place_order(item="bearings"),
)
pipeline.print_report(result)
# Shows: delegation_check -> scope_engine -> semantic_verifier -> budget -> pattern -> escalation -> execution
// TypeScript
import { AgentCheckClient, VerificationPipeline, ClaudeProvider, BudgetTracker } from "agentcheck-sdk";

const pipeline = new VerificationPipeline(client, {
  llm: new ClaudeProvider(),
  budget: new BudgetTracker({ dailyLimit: 50000 }),
});

const result = await pipeline.verifyAndExecute(
  "factory-bot", "order_parts",
  () => placeOrder({ item: "bearings" }),
  { amount: 3000 },
);

Known Limitations (Honest Disclosure)

AgentCheck is transparent about what it can and cannot do.

AgentCheck는 할 수 있는 것과 없는 것을 솔직하게 공개합니다.

Can detectCannot detect
Unauthorized action (not in allowed list)Malicious intent behind allowed action
Amount over limit ($15K > $10K)Unnecessary purchase within limit
Cumulative budget exceededData exfiltration through allowed reads
Unusual frequency (anomaly)Prompt injection in agent system
Denied action attemptedSubtle scope abuse matching exact wording

Mitigation for what we cannot detect:

LimitationMitigation
Malicious intentSemanticVerifier (LLM) flags "suspicious" for human review
Cumulative small abuseBudgetTracker catches total exceeding daily/monthly limits
Unusual patternsPatternMonitor detects frequency/amount anomalies
High-risk actionsHumanEscalation requires human approval above threshold
All of the above combinedVerificationPipeline chains all layers sequentially
Our philosophy: No verification system is perfect. We provide multiple layers that each reduce risk. When our automated checks are uncertain, we escalate to humans. We are transparent about our limitations because hiding them would undermine the trust we are trying to build.

우리의 철학: 완벽한 검증 시스템은 없습니다. 여러 레이어로 위험을 줄입니다. 자동 검증이 불확실하면 사람에게 넘깁니다. 한계를 숨기면 신뢰가 무너지기 때문에 솔직하게 공개합니다.

Rate Limits

EndpointLimit
POST /api/v1/signup3 per IP per hour
POST /api/v1/record100 per minute per API key
GET /api/v1/record/:id300 per minute per API key
GET /api/v1/records60 per minute per API key
GET /api/v1/verify/:idNo limit (public)

API Endpoints

POST /api/v1/signup
Create account and get API key. No auth required.
POST /api/v1/record
Create a new agreement. Sends approval email. Requires X-API-Key.
GET /api/v1/record/:id
Get agreement details. Requires X-API-Key.
GET /api/v1/records
List agreements with optional filters. Requires X-API-Key.
POST /api/v1/record/:id/amend
Amend agreement scope. Requires X-API-Key.
POST /api/v1/record/:id/revoke
Revoke agreement immediately. Requires X-API-Key. Body: {"reason": "..."}
POST /api/v1/webhooks
Register a webhook endpoint. Requires X-API-Key.
GET /api/v1/verify/:id
Public verification page (HTML). No auth required.
GET /api/v1/approve/:token
Approve agreement via email link. No auth required.

SDK Reference

Individual Commands (Basic Menu / 개별 명령어)

Low-level API calls. Use these when you need full control.

개별 API 호출. 세밀한 제어가 필요할 때 사용합니다.

PythonTypeScriptDescription
agentcheck.record()client.record()Create agreement + send email / 위임장 생성
agentcheck.get(id)client.get(id)Get agreement by ID / 위임장 조회
agentcheck.list()client.list()List with filters / 목록 조회 (필터)
agentcheck.amend()client.amend()Amend scope / 위임장 수정
agentcheck.revoke()client.revoke()Revoke immediately / 즉시 폐기
Client.signup()Client.signup()Self-service signup / 회원가입
client.register_webhook()client.registerWebhook()Register webhook / 웹훅 등록

Set Menus (High-Level Wrappers / 세트 메뉴)

Pre-built combinations of individual commands. Use these for common patterns.

개별 명령어를 조합한 고수준 래퍼. 일반적인 패턴에 바로 사용합니다.

1. DelegationProvider - Universal (모든 프로젝트)

Combines permission check + scope verification + execution logging into simple calls.

권한 확인 + 범위 검증 + 실행 로깅을 간단한 호출로 조합합니다.

# Python
from agentcheck import Client, DelegationProvider

client = Client(api_key="ak_live_...")
provider = DelegationProvider(client,
    verify_scope=lambda scope, action: action in scope
)

# Can this agent do this action?
result = provider.can_act("my-bot", "send-email")
# {"allowed": True, "agreement": ...}

# Does this agent have any valid delegation?
provider.has_active_delegation("my-bot")  # True/False

# Log what agent did
provider.log_execution("my-bot", "send-email", result="success")

# Check + execute + log in one call
provider.execute_with_guard("my-bot", "send-email", lambda: send_email())
// TypeScript
import { AgentCheckClient, DelegationProvider } from "agentcheck-sdk";

const client = new AgentCheckClient("ak_live_...");
const provider = new DelegationProvider(client, {
  verifyScope: (scope, action) => scope.includes(action),
});

await provider.canAct("my-bot", "send-email");
await provider.hasActiveDelegation("my-bot");
await provider.logExecution("my-bot", "send-email");
await provider.executeWithGuard("my-bot", "send-email", () => sendEmail());

2. DelegationGuard - Express / FastAPI Middleware

Automatically checks delegation before every API request. Zero code in your route handlers.

모든 API 요청 전에 자동으로 위임 확인. 라우트 핸들러에 코드 추가 불필요.

// TypeScript (Express)
import { delegationGuard } from "agentcheck-sdk";

app.use('/api/agent/*', delegationGuard(provider, {
  getAgent: (req) => req.headers['x-agent-id'],
  getAction: (req) => `${req.method} ${req.path}`,
}));
// All agent routes are now automatically protected
# Python (FastAPI)
from agentcheck.guard import fastapi_guard

@app.middleware("http")
async def check(request, call_next):
    return await fastapi_guard(
        provider,
        get_agent=lambda req: req.headers.get("x-agent-id"),
        get_action=lambda req: f"{req.method} {req.url.path}",
    )(request, call_next)

3. AgentToolChecker - LangChain / CrewAI

Wrap any LangChain tool with automatic delegation checks. The agent can only use tools it's authorized for.

LangChain 도구에 자동 위임 체크를 래핑. 에이전트는 승인된 도구만 사용할 수 있습니다.

# Python
from agentcheck import AgentToolChecker

checker = AgentToolChecker(provider, "my-bot")

# Wrap existing tools - they now require delegation
safe_send = checker.wrap("send-email", original_send_email)
safe_query = checker.wrap("query-db", original_query_db)

# These will check delegation, execute, and log automatically
safe_send(to="[email protected]", body="Hello")
safe_query(sql="SELECT * FROM orders")
// TypeScript
import { AgentToolChecker } from "agentcheck-sdk";

const checker = new AgentToolChecker(provider, "my-bot");
const safeSend = checker.wrap("send-email", originalSendEmail);
await safeSend({ to: "[email protected]" });
// Delegation checked + executed + logged

4. DelegationDashboard - Embeddable Widget

Drop a live delegation status widget into your admin page. Auto-refreshing, self-contained HTML.

관리자 페이지에 위임 현황 위젯을 삽입합니다. 자동 갱신, 독립적 HTML.

# Python (FastAPI)
from agentcheck import Client, DelegationDashboard
from fastapi.responses import HTMLResponse

client = Client(api_key="ak_live_...")
dashboard = DelegationDashboard(client, title="Our Agents", refresh_interval=30)

@app.get("/admin/delegations")
def delegations():
    return HTMLResponse(dashboard.render())

# Or embed in existing page:
widget_html = dashboard.render_widget()  # HTML snippet only
// TypeScript (Express)
import { AgentCheckClient, DelegationDashboard } from "agentcheck-sdk";

const dashboard = new DelegationDashboard(client, {
  title: "Our Agents",
  refreshInterval: 30,
});

app.get("/admin/delegations", async (req, res) => {
  res.send(await dashboard.render());
});

// Or embed widget in your template:
const widget = await dashboard.renderWidget();

Summary: Individual vs Set Menu

When to useIndividual (Basic)Set Menu (Wrapper)
You want full controlUse this
Quick integrationUse this
Custom approval flowUse this
Standard check+execute+logUse this
Middleware (Express/FastAPI)DelegationGuard
LangChain/CrewAI toolsAgentToolChecker

Error Codes

CodeHTTPDescription
unauthorized401Missing or invalid API key
not_found404Agreement not found
bad_request400Invalid request parameters
rate_limited429Too many requests
internal_error500Server error