Intaglio Documentation
Policy enforcement and a tamper-evident audit trail for AI agents that move money. Deterministic rule evaluation, a SHA-256 hash chain, and optional anchoring on Solana devnet.
#Quick Start
One HTTP endpoint, no SDK required. Every decision gets a SHA-256 chain link and a stored audit record.
intg_live_… and are shown once.POST /api/enforce before the agent executes. Intaglio returns APPROVE, DENY, or REQUIRE_APPROVAL. Execute only on APPROVE.# 1–2. Create the agent, set its limits and mint a key in the dashboard
export INTAGLIO_API_KEY=intg_live_...
# 3. Enforce an action before your agent executes it
curl -X POST https://solanacompliance.com/api/enforce \
-H "Authorization: Bearer $INTAGLIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_slug":"my-agent","action":{"type":"transfer","amount":100,"currency":"USDC","destination":"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJevxJSw"}}'
# 4. Anyone can check a decision by its hash — no key needed
curl https://solanacompliance.com/api/verify/<hash>Fail closed. Every error response is itself a DENY decision, so an agent that executes only on APPROVE can never spend on an outage.
#Authentication
POST /api/enforce requires a Bearer token in the Authorization header. Each key belongs to one operator and can only enforce for that operator's agents.
| Key | Prefix | Use |
|---|---|---|
| API key | intg_live_ | Enforcement for your agents. Revoke any time in Settings → API Keys. |
GET /api/verify/{hash} is public and needs no key. Everything else — agents, limits, approvals, audit export — is managed in the dashboard with your signed-in session.
API keys are shown once on creation. Store them in environment variables — never commit them to source control.
#Enforce an Action
The core primitive. Every agent action passes through this endpoint before execution. Rule evaluation is deterministic and typically takes well under a millisecond (enforcement_ms); end-to-end latency also includes OFAC screening and storage (total_ms).
Request
curl -X POST https://solanacompliance.com/api/enforce \
-H "Authorization: Bearer $INTAGLIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_slug": "my-defi-agent",
"action": {
"type": "transfer",
"amount": 4200,
"currency": "USDC",
"destination": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJevxJSw"
}
}'| Field | Type | Description |
|---|---|---|
agent_slug | string | Required. Letters, digits and dashes, max 64 chars. Must belong to the key's operator. |
action.type | string | Required. Free-form action label, e.g. transfer or payment. |
action.amount | number | Required. JSON number from 0 to 1,000,000,000. Strings are rejected. |
action.currency | string | Required. USDC, USDT or SOL. Must match the policy currency (see Policy Language). |
action.destination | string | Required. Recipient address, max 100 chars. Screened against OFAC. |
action.metadata | object | Optional. Stored with the record. |
Response — DENY
{
"outcome": "DENY",
"reason": "Transaction amount 12000 USDC exceeds daily limit of 10000 USDC",
"rule_triggered": "hard_block",
"hash": "sha256:5d1e…",
"prev_hash": "sha256:9f2c…",
"enforcement_ms": 0.38,
"ofac_ms": 21.4,
"total_ms": 71,
"audit_record_id": "b0c4…",
"approval_request_id": null,
"policy_found": true,
"risk_score": 70,
"risk_level": "high",
"kya_flags": ["LARGE_TRANSACTION"],
"solana_tx": null,
"solana_explorer": null
}Response — APPROVE
{
"outcome": "APPROVE",
"reason": "Within policy limits",
"rule_triggered": "auto_approve",
"hash": "sha256:9f2c…",
"prev_hash": "sha256:41ab…",
"enforcement_ms": 0.41,
"ofac_ms": 19.8,
"total_ms": 66,
"audit_record_id": "3f6c…",
"approval_request_id": null,
"policy_found": true,
"risk_score": 0,
"risk_level": "low",
"kya_flags": [],
"solana_tx": null,
"solana_explorer": null
}Response — REQUIRE_APPROVAL
{
"outcome": "REQUIRE_APPROVAL",
"reason": "Transaction amount 4200 USDC exceeds auto-approval threshold of 1000 USDC",
"rule_triggered": "require_human_review",
"hash": "sha256:c8d5…",
"prev_hash": "sha256:5d1e…",
"enforcement_ms": 0.44,
"ofac_ms": 20.1,
"total_ms": 69,
"audit_record_id": "7a91…",
"approval_request_id": "e2f0…",
"policy_found": true,
"risk_score": 40,
"risk_level": "medium",
"kya_flags": ["LARGE_TRANSACTION", "APPROVAL_REQUIRED"],
"solana_tx": null,
"solana_explorer": null
}Every stored decision returns hash and prev_hash, forming a hash chain. Responses also carry kernel_* fields: a Rust kernel evaluates the same action in shadow mode and reports whether it agrees. It does not decide. Repeating an identical request within 5 minutes returns the original decision with "duplicate": true instead of recording a second one.
#Outcomes
Every enforcement returns exactly one of three outcomes.
rule_triggered says why.Rule codes (rule_triggered)
| Code | Outcome | Meaning |
|---|---|---|
| auto_approve | APPROVE | Amount at or under the auto-approve limit |
| require_human_review | REQUIRE_APPROVAL | Amount above the auto-approve limit and under the hard cap |
| hard_block | DENY | Amount above the hard cap |
| allowlist_only | DENY | Destination not on the agent's allowlist |
| ofac_screening | DENY | Destination matched the OFAC SDN list |
| currency_out_of_scope | DENY | Action currency differs from the currency the limits are set in |
| input_validation | DENY | Malformed request (HTTP 400) |
| auth | DENY | Missing, invalid or out-of-scope key (HTTP 401/403/503) |
| rate_limit | DENY | Too many requests (HTTP 429) |
| policy_lookup / decision_capture / system_error | DENY | Backend unavailable — failing closed (HTTP 409/500/503) |
#Policy Language (APL)
Agent Policy Language is a deterministic, human-readable DSL for spending limits and approval thresholds. Dashboard → Limits generates the policy below; editing limits there is the supported way to change them.
policy "my-defi-agent-v2" {
version = "2.0"
rule auto_approve {
when input.amount <= 500
then APPROVE
}
rule require_human_review {
when input.amount > 1000 and input.amount <= 10000
then REQUIRE_APPROVAL
reason "Amount requires human review"
}
rule daily_limit {
when daily_total(input) > 10000
then DENY
reason "Daily limit of 10000 USDC exceeded"
}
rule allowlist_only {
when !(input.destination in ["7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJevxJSw"])
then DENY
reason "Destination not on allowlist"
}
}| Setting | How the hosted endpoint applies it |
|---|---|
| Auto-approve limit | APPROVE at or under this amount (rule auto_approve). |
| Approval threshold | REQUIRE_APPROVAL above this amount (rule require_human_review). |
| Hard cap | DENY above this amount. It is also the daily limit: once the total approved today (UTC) would pass it, further actions are denied (rule daily_limit_exceeded). |
| Allowlist | If set, DENY any destination not on the list. |
| Currency | Limits are in USDC. To use another currency, name it after the auto-approve amount, e.g. when input.amount <= 2 SOL. Actions in any other currency are denied. |
| OFAC screening | Always on. Destinations on the OFAC SDN list are denied. |
Full grammar. The complete APL spec — scope, limit, require and obligation blocks — is implemented by the reference engine and the Rust kernel, and the intaglio.policy.json schema is published under CC-BY-4.0. The hosted endpoint currently enforces only the settings above. View schema →
#Hash Chain
Every stored decision carries its own hash and the prev_hash of the decision before it. Each hash commits to the previous one, so changing any record breaks every link after it. Stored audit records cannot be updated or deleted — the database rejects both.
Verify a record
# Public — no API key. Accepts the full hash or a >= 8-char hex prefix.
curl https://solanacompliance.com/api/verify/sha256:9f2c...
# {
# "verified": true,
# "hash": "sha256:9f2c…",
# "prev_hash": "sha256:41ab…",
# "outcome": "APPROVE",
# "solana_tx": "4vhK…",
# "explorer_url": "https://explorer.solana.com/tx/4vhK…?cluster=devnet",
# …
# }#Solana Anchoring
When anchoring is enabled, each decision's hash is written to Solana devnet through the Memo program, asynchronously after the response is returned. Devnet is a public test network: it gives an independent timestamp anyone can check, but it is not a production ledger. Mainnet anchoring is on the roadmap and not live.
| Field | Value |
|---|---|
| Cluster | devnet |
| Memo program | MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr |
| Memo content | intaglio:sha256:{hash} |
| Finding a record's transaction | GET /api/verify/{hash} → solana_tx, explorer_url (once confirmed) |
| solana_tx in the enforce response | The previous decision's anchor, if already confirmed. Look up a new decision's own anchor via /api/verify a few seconds later. |
Independent check: any Solana explorer set to devnet can confirm a memo exists for a hash — no Intaglio account required.
#Webhooks
DENY and REQUIRE_APPROVAL decisions trigger a notification to the deployment's webhook URL (Slack incoming-webhook URLs get a formatted message; any other URL gets the JSON below). Per-operator webhook URLs are not self-serve yet — ask us to route them.
Webhook payload
// POST → the deployment's notification URL, for DENY and REQUIRE_APPROVAL
{
"event": "REQUIRE_APPROVAL",
"agent_slug": "my-defi-agent",
"action": {
"type": "transfer",
"amount": 4200,
"currency": "USDC",
"destination": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJevxJSw"
},
"hash": "sha256:c8d5…",
"audit_record_id": "7a91…",
"dashboard_url": "https://…/app/pending",
"timestamp": "2026-09-28T11:44:22.887Z"
}Responding to approvals
Approve or deny pending actions in Dashboard → Pending. Approvals are resolved by a signed-in operator; there is no API-key endpoint for them yet. Your agent should hold the action until it sees the outcome there, or re-submit it later.
#Audit Trail
Every decision made with an API key is stored as an append-only audit record, visible only to members of your operator. Browse it in Dashboard → Audit, or download it while signed in.
| Where | What you get |
|---|---|
| Dashboard → Audit | Paginated records per agent, with outcome, reason, rule and hashes. |
| /api/audit/export?format=csv|json|html | Latest 100 records for your operator. Requires a signed-in session. |
| /verify/{hash} | Public, read-only view of a single decision for auditors and counterparties. |
#Compliance
MiCA Art. 68(9) requires crypto-asset service providers to keep records of every service, activity, order, and transaction for 5 years — up to 7 on regulator request — and to produce them on demand. When an autonomous agent places the order, conventional application logging does not produce a record that survives an audit. Intaglio produces one at decision time.
| Requirement | What Intaglio provides |
|---|---|
| MiCA Art. 68(9) — record-keeping | An append-only, hash-chained record for every enforced action, exportable on demand. Records cannot be edited or deleted once written. |
| OFAC SDN screening | Every destination is screened before a decision; matches are denied. |
| Tamper evidence | SHA-256 hash chain; optional Solana devnet anchoring for an independent timestamp. |
| Data residency | Audit records are stored in the EU (Supabase, AWS eu-west-1, Ireland). |
| EU AI Act Art. 12 — logging | Applies to high-risk AI systems from 2 December 2027. The decision log is designed to support it; it is not a certification. |
Checkable without us. Anyone with a decision hash can look it up at /verify/{hash}and, when anchored, confirm the memo on a devnet explorer.
#Errors
Errors from /api/enforce are returned as a DENY decision, so a caller that executes only on APPROVE fails safe. Where available, code gives a stable machine-readable reason.
| HTTP status | rule_triggered / code | Meaning |
|---|---|---|
| 400 | input_validation | Malformed JSON, invalid agent_slug, missing field, non-numeric amount, or unsupported currency. |
| 401 | auth | Missing, invalid or revoked API key. |
| 403 | auth · AUTH_INSUFFICIENT_SCOPE / AUTH_AGENT_MISMATCH / AUTH_AGENT_NOT_FOUND | Key lacks enforce scope, or the agent does not belong to the key's operator. |
| 409 | policy_lookup · POLICY_NOT_FOUND | The agent has no active policy. Set its limits in the dashboard. |
| 429 | rate_limit | More than 300 requests per minute for one agent, or 100 per minute from one IP. See X-RateLimit-* and Retry-After headers. |
| 500 | decision_capture / system_error | The decision could not be recorded or an unexpected error occurred — failing closed. |
| 503 | auth / policy_lookup · AUTH_BACKEND_UNAVAILABLE / POLICY_LOOKUP_FAILED | A backend is unavailable — failing closed. Retry later. |
{
"outcome": "DENY",
"reason": "Rate limit exceeded — 300 requests per minute per agent",
"rule_triggered": "rate_limit"
}Questions not covered here? Email the team or request source access.