Get started with Ardyn.
From zero to your first governed agent action in under five minutes. Works with any agent framework — OpenAI, xAI, Anthropic, LangGraph, CrewAI, n8n, or plain Python.
Go to ardyn.ai/start and complete the four-step registration:
export ARDYN_API_KEY="ardyn_live_..."
export ARDYN_TENANT_ID="your-tenant-id" # shown at step 4Create labeled sub-keys for individual agents or machines. Revoke any key instantly, no restart required.
curl -X POST https://api.ardyn.ai/v1/keys \
-H "Authorization: Bearer $ARDYN_API_KEY" \
-H "content-type: application/json" \
-d '{"label": "invoice-agent-prod-01"}'The raw key is returned once. Manage all keys at ardyn.ai/console — Panel 4.
All integrations use the same ardyn_governance.py helper. Copy it once, use it everywhere.
ardyn_governance.py — shared helper (all providers)
<span class="kw">import urllib.request, json, hashlib, os, logging
logger = logging.<span class="fn">getLogger(<span class="str">"ardyn")
ARDYN_KEY = os.environ[<span class="str">"ARDYN_API_KEY"]
ARDYN_TENANT = os.environ[<span class="str">"ARDYN_TENANT_ID"]
GATEWAY = <span class="str">"https://api.ardyn.ai"
<span class="kw">def <span class="fn">_hash(s):
<span class="kw">try:
<span class="kw">import blake3; <span class="kw">return blake3.blake3(s.encode()).hexdigest()
<span class="kw">except ImportError:
<span class="kw">return hashlib.blake2b(s.encode()).hexdigest()
<span class="kw">def <span class="fn">govern_and_attest(tool_name, tool_input, tool_result, agent_id=<span class="str">"my-agent"):
<span class="cmt">"""Govern + attest in a single call. POST /v1/attest evaluates policy AND mints the AEA.
<span class="cmt"> Returns {'decision': 'ALLOW', 'ddc_id': '...', 'receipt': {...}} on success.
<span class="cmt"> Returns {'decision': 'BLOCK', 'reason': '...'} on governance rejection.
<span class="cmt"> """
<span class="kw">try:
data = json.dumps({
<span class="str">"api_key": ARDYN_KEY,
<span class="str">"tool_name": tool_name,
<span class="str">"input_hash": <span class="fn">_hash(json.dumps(tool_input, sort_keys=<span class="kw">True)),
<span class="str">"output_hash": <span class="fn">_hash(str(tool_result)),
<span class="str">"metadata": {<span class="str">"agent_id": agent_id},
}).encode()
req = urllib.request.Request(
f<span class="str">"{GATEWAY}/v1/attest", data=data,
headers={<span class="str">"Authorization": f<span class="str">"Bearer {ARDYN_KEY}",
<span class="str">"Content-Type": <span class="str">"application/json"},
method=<span class="str">"POST",
)
<span class="kw">with urllib.request.urlopen(req, timeout=<span class="num">10) <span class="kw">as r:
resp = json.loads(r.read())
resp[<span class="str">"decision"] = <span class="str">"ALLOW"
logger.<span class="fn">info(<span class="str">"Ardyn ALLOWED: tool=%s ddc_id=%s", tool_name, resp.get(<span class="str">"ddc_id"))
<span class="kw">return resp
<span class="kw">except urllib.error.HTTPError <span class="kw">as e:
<span class="kw">if e.code == <span class="num">403:
body = json.loads(e.read())
error_data = json.loads(body.get(<span class="str">"error", <span class="str">"{}"))
reason = error_data.get(<span class="str">"reason", <span class="str">"policy_blocked")
logger.<span class="fn">warning(<span class="str">"Ardyn BLOCKED: tool=%s reason=%s", tool_name, reason)
<span class="kw">return {<span class="str">"decision": <span class="str">"BLOCK", <span class="str">"reason": reason, <span class="str">"ddc_id": <span class="kw">None}
<span class="kw">except Exception <span class="kw">as e:
logger.<span class="fn">error(<span class="str">"Ardyn unreachable — FAIL-CLOSED (tool blocked): %s", e)
<span class="kw">return {<span class="str">"decision": <span class="str">"BLOCK", <span class="str">"reason": <span class="str">"ardyn_unreachable", <span class="str">"ddc_id": <span class="kw">None}Integration by provider
Build from source
git clone https://github.com/Kylewilson04/ardyn.git
cd ardyn
cargo build --release -p ardyn-cli
# Binary: ./target/release/ardyn-cli
sudo cp target/release/ardyn-cli /usr/local/bin/ardyn
ardyn --version
# ardyn 0.2.01. Register your organization
$ ardyn init
Ardyn CLI — Setup
──────────────────────────
Email address: kyle@acme.ai
Organization name: Acme Insurance
Your name: Kyle Wilson
✓ Organization created
Tenant ID: 3cf4f022-3e3e-45f1-8528-158434f9bcbc
API Key: REDACTED_SECRET_REMOVED
Config saved to ~/.ardyn/config.json
Run 'ardyn agent add' to connect your first agent.2. Add an agent
$ ardyn agent add
Agent name (e.g. invoice-agent-prod): invoice-agent
Framework:
1) Python / REST (generic)
2) OpenAI / ChatGPT
3) xAI / Grok
4) Anthropic / Claude
5) LangGraph
6) CrewAI
7) n8n
8) MCP Proxy
Select (1-8): 2
Environment (production/development/staging): production
✓ Key created: REDACTED_SECRET_REMOVED
⚠ Copy this key — it will not be shown again.
✓ ardyn_governance.py written to ./ardyn_governance.py
Add these 3 lines to your OpenAI dispatch function:
from ardyn_governance import govern_and_attest
aea = govern_and_attest(name, args, result, AGENT_ID)
if aea.get("decision") != "ALLOW": print(f"BLOCKED: {aea.get("reason")}")
Docs: https://ardyn.ai/docs/install.html3. Verify any receipt
$ ardyn verify 40c2d8eb-1d4b-4e34-b642-f99e8e79af88
┌──────────────────────────────────────────────┐
│ AEA Verification │
│ │
│ AEA ID: 40c2d8eb-1d4b-4e34-b642-f99e8... │
│ Signature: ✓ Valid │
│ Chain: ✓ Intact (sequence 4673) │
│ Tier: tier_1 (Bronze) │
│ Tool: terminal │
│ Tenant: 3cf4f022-3e3e-45f1-8528-15843... │
│ Timestamp: 2026-06-24 18:12:09 UTC │
└──────────────────────────────────────────────┘
$ ardyn verify --file ./receipt.json
# Same output — reads ddc_id from JSON filefrom openai import OpenAI
import json
from ardyn_governance import govern_and_attest
client = OpenAI(api_key="your-openai-key")
AGENT_ID = "openai-agent-01"
def dispatch_tool(name, args):
if name == "approve_invoice":
return {"status": "approved", "invoice_id": args["invoice_id"]}
raise ValueError(f"Unknown tool: {name}")
def run_governed_agent(messages, tools):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)
message = response.choices[0].message
for tool_call in (message.tool_calls or []):
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
gov = govern_and_attest(name, args, result, AGENT_ID)
if aea.get("decision") == "BLOCK":
result = {"error": "blocked_by_ardyn", "reason": aea.get("reason")}
else:
result = dispatch_tool(name, args)
, AGENT_ID)
print(f"Tool: {name} | Decision: {aea.get("decision")} | Result: {result}")
return messagefrom openai import OpenAI
import json
from ardyn_governance import govern_and_attest
client = OpenAI(
api_key="your-xai-key",
base_url="https://api.x.ai/v1",
)
AGENT_ID = "xai-agent-01"
def run_governed_agent(messages, tools):
response = client.chat.completions.create(
model="grok-3",
messages=messages,
tools=tools,
)
message = response.choices[0].message
for tool_call in (message.tool_calls or []):
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
gov = govern_and_attest(name, args, result, AGENT_ID)
if aea.get("decision") == "BLOCK":
result = {"error": "blocked_by_ardyn", "reason": aea.get("reason")}
else:
result = dispatch_tool(name, args)
, AGENT_ID)
return messageimport anthropic
from ardyn_governance import govern_and_attest
client = anthropic.Anthropic(api_key="your-anthropic-key")
AGENT_ID = "claude-agent-01"
def run_governed_agent(messages, tools):
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
name = block.name
args = block.input
gov = govern_and_attest(name, args, result, AGENT_ID)
if aea.get("decision") == "BLOCK":
result = {"error": "blocked_by_ardyn", "reason": aea.get("reason")}
else:
result = dispatch_tool(name, args)
, AGENT_ID)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
})
return tool_resultsfrom langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
from ardyn_governance import govern_and_attest
import functools
AGENT_ID = "langgraph-agent-01"
def ardyn_governed(fn):
"""Decorator: wrap any LangChain tool with Ardyn governance."""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
tool_input = {"args": args, **kwargs}
gov = govern_and_attest(fn.__name__, tool_input, result, AGENT_ID)
if aea.get("decision") == "BLOCK":
return f"Action blocked by governance: {aea.get('reason')}"
result = fn(*args, **kwargs)
, AGENT_ID)
return result
return wrapper
@tool
@ardyn_governed
def approve_invoice(invoice_id: str, amount: float) -> dict:
"""Approve an invoice for payment."""
return {"approved": True, "invoice_id": invoice_id, "amount": amount}
@tool
@ardyn_governed
def send_payment(vendor_id: str, amount: float) -> dict:
"""Send a payment to a vendor."""
return {"sent": True, "vendor_id": vendor_id, "amount": amount}
tools = [approve_invoice, send_payment]
tool_node = ToolNode(tools)from crewai import Agent, Task, Crew
from crewai.tools import BaseTool
from ardyn_governance import govern_and_attest
from pydantic import BaseModel
from typing import Type
AGENT_ID = "crewai-agent-01"
class ArdynToolWrapper(BaseTool):
"""Base class: wraps any CrewAI tool with Ardyn governance."""
ardyn_tool_fn: callable = None
def _run(self, **kwargs) -> str:
name = self.name
tool_input = kwargs
gov = govern_and_attest(name, tool_input, result, AGENT_ID)
if aea.get("decision") == "BLOCK":
return f"Blocked: {aea.get('reason')}"
result = self.ardyn_tool_fn(**kwargs)
, aea.get("trace_id"), AGENT_ID)
return str(result)
class ApproveInvoiceTool(ArdynToolWrapper):
name: str = "approve_invoice"
description: str = "Approve an invoice for payment"
ardyn_tool_fn: callable = lambda self, **kw: {"approved": True, **kw}
finance_agent = Agent(
role="Finance Automation Agent",
goal="Process invoices within policy",
tools=[ApproveInvoiceTool()],
verbose=True,
)Option A — HTTP Request nodes (no-code)
Node: HTTP Request (place AFTER your action node)
Method: POST
URL: https://api.ardyn.ai/v1/attest
Authentication: Header Auth
Name: Authorization
Value: Bearer {{ $env.ARDYN_API_KEY }}
Body (JSON):
{
"api_key": "{{ $env.ARDYN_API_KEY }}",
"tool_name": "{{ $node['Previous Node'].name }}",
"input_hash": "{{ $node['Previous Node'].json | hash }}",
"output_hash": "{{ $json | hash }}"
}
Then: IF node
Condition: {{ $json.decision }} equals "BLOCK"
True branch: Stop workflow — governance rejected this action
False branch: Continue (AEA minted)Option B — Code node (for attestation)
// Ardyn attestation — place AFTER your action node (governance + AEA in one call)
const ARDYN_KEY = $env.ARDYN_API_KEY;
const toolName = "send_payment";
const prevNode = $input.first().json;
const response = await $http.post({
url: "https://api.ardyn.ai/v1/attest",
headers: { "Authorization": `Bearer ${ARDYN_KEY}` },
body: {
api_key: ARDYN_KEY,
tool_name: toolName,
input_hash: sha256(JSON.stringify(prevNode)),
output_hash: sha256(JSON.stringify(prevNode)),
},
});
if (response.decision === "BLOCK") {
throw new Error(`Ardyn BLOCKED this action: ${response.reason}`);
}
return [{ json: { ...prevNode, ddc_id: response.ddc_id } }];// Attestation node — place after your action node
const ddcId = $input.first().json.ardyn_ddc_id;
const result = $input.first().json;
await $http.post({
url: "https://api.ardyn.ai/v1/attest",
headers: { "Authorization": `Bearer ${$env.ARDYN_API_KEY}` },
body: {
api_key: $env.ARDYN_API_KEY,
tool_name: "send_payment",
input_hash: btoa(JSON.stringify($input.first().json)),
output_hash: btoa(JSON.stringify(result)),
metadata: { trace_id: ddcId, agent_id: "n8n-workflow" },
},
});
return [{ json: result }];Build the proxy
git clone https://github.com/Kylewilson04/ardyn.git
cd ardyn
cargo build --release -p ardyn-mcp-proxy
# Binary: ./target/release/ardyn-mcp-proxyCreate a config file per MCP server
upstream_command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/your/path"]
gateway_url = "https://api.ardyn.ai"
api_key = "ardyn_live_..."
tenant_id = "your-tenant-id"
axiom_governance_url = "https://api.ardyn.ai"
axiom_api_key = "ardyn_live_..."
inject_metadata = true
log_level = "info"Wire into Claude Desktop
{
"mcpServers": {
"filesystem-governed": {
"command": "/path/to/ardyn/target/release/ardyn-mcp-proxy",
"args": ["--config", "/Users/you/.ardyn/proxy-filesystem.toml"]
}
}
}Browser
Go to ardyn.ai/receipt-verify. Paste any AEA ID. Verification runs client-side — no servers contacted. Try a real receipt: 40c2d8eb-1d4b-4e34-b642-f99e8e79af88
API
curl https://api.ardyn.ai/v1/verify/40c2d8eb-1d4b-4e34-b642-f99e8e79af88Key management
Create one sub-key per agent or machine. Revoke instantly — no restart required.
# Create
curl -X POST https://api.ardyn.ai/v1/keys \
-H "Authorization: Bearer $ARDYN_API_KEY" \
-H "content-type: application/json" \
-d '{"label": "invoice-agent-prod-01"}'
# List
curl https://api.ardyn.ai/v1/keys \
-H "Authorization: Bearer $ARDYN_API_KEY"
# Revoke immediately
curl -X DELETE https://api.ardyn.ai/v1/keys/ardyn_live_... \
-H "Authorization: Bearer $ARDYN_API_KEY"Endpoint reference
| Endpoint | Auth | Purpose |
|---|---|---|
| POST /v1/attest | Bearer key | Govern + attest a tool call — evaluates policy, returns verdict, mints verifiable AEA |
| GET /v1/verify/:id | Public | Verify any AEA independently |
| GET /gateway/v1/trust/:agent_id | Bearer key | Agent trust profile — standing, AEA count, authority |
| POST /v1/keys | Bearer key | Create labeled sub-key |
| GET /v1/keys | Bearer key | List all keys (raw values never returned) |
| DELETE /v1/keys/:id | Bearer key | Revoke key — immediate effect |
| GET /health | Public | Gateway health check |
The Console
ardyn.ai/console — four panels, no setup:
- Agent Trust ProfileTrust standing, AEA count, authority level, first/last seen for any agent ID.
- Receipt VerifierPaste any AEA ID. Signature, hashes, governance decision, chain validity.
- Live GovernanceFire a governance evaluate call and watch the nine-layer trace in real time.
- API KeysCreate sub-keys, view last-used timestamps, revoke in one click.