v0.1 · Public Beta

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.

1
Create an agent and set its limits
Sign in, add an agent under Dashboard → Agents, then set its auto-approve limit, approval threshold, hard cap and allowlist under Dashboard → Limits.
2
Mint an API key
Dashboard → Settings → API Keys. Keys look like intg_live_… and are shown once.
3
Enforce every action
Call 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.

KeyPrefixUse
API keyintg_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"
    }
  }'
FieldTypeDescription
agent_slugstringRequired. Letters, digits and dashes, max 64 chars. Must belong to the key's operator.
action.typestringRequired. Free-form action label, e.g. transfer or payment.
action.amountnumberRequired. JSON number from 0 to 1,000,000,000. Strings are rejected.
action.currencystringRequired. USDC, USDT or SOL. Must match the policy currency (see Policy Language).
action.destinationstringRequired. Recipient address, max 100 chars. Screened against OFAC.
action.metadataobjectOptional. Stored with the record.

Response — DENY

Response 200 · outcome: DENY (example values)
{
  "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

Response 200 · outcome: APPROVE (example values)
{
  "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

Response 200 · outcome: REQUIRE_APPROVAL (example values)
{
  "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.

APPROVE
Action permitted
At or under the auto-approve limit and no rule blocked it. Execute.
DENY
Action blocked
A hard rule, sanctions match or error. Do not execute. rule_triggered says why.
REQUIRE_APPROVAL
Human review
Above the auto-approve limit, under the hard cap. Hold the action; resolve it in Dashboard → Pending.

Rule codes (rule_triggered)

CodeOutcomeMeaning
auto_approveAPPROVEAmount at or under the auto-approve limit
require_human_reviewREQUIRE_APPROVALAmount above the auto-approve limit and under the hard cap
hard_blockDENYAmount above the hard cap
allowlist_onlyDENYDestination not on the agent's allowlist
ofac_screeningDENYDestination matched the OFAC SDN list
currency_out_of_scopeDENYAction currency differs from the currency the limits are set in
input_validationDENYMalformed request (HTTP 400)
authDENYMissing, invalid or out-of-scope key (HTTP 401/403/503)
rate_limitDENYToo many requests (HTTP 429)
policy_lookup / decision_capture / system_errorDENYBackend 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.

my-defi-agent.apl
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"
  }
}
SettingHow the hosted endpoint applies it
Auto-approve limitAPPROVE at or under this amount (rule auto_approve).
Approval thresholdREQUIRE_APPROVAL above this amount (rule require_human_review).
Hard capDENY 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).
AllowlistIf set, DENY any destination not on the list.
CurrencyLimits 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 screeningAlways 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.

Record #1
transfer 50 USDC
APPROVE
hash: sha256:aaa111...
prev: sha256:000... (genesis)
Record #2
transfer 200 USDC
APPROVE
hash: sha256:bbb222...
prev: sha256:aaa111...
Record #3
transfer 12,000 USDC
DENY
hash: sha256:ccc333...
prev: sha256:bbb222...

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.

FieldValue
Clusterdevnet
Memo programMemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr
Memo contentintaglio:sha256:{hash}
Finding a record's transactionGET /api/verify/{hash} → solana_tx, explorer_url (once confirmed)
solana_tx in the enforce responseThe 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 → your webhook URL
// 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.

WhereWhat you get
Dashboard → AuditPaginated records per agent, with outcome, reason, rule and hashes.
/api/audit/export?format=csv|json|htmlLatest 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.

RequirementWhat Intaglio provides
MiCA Art. 68(9) — record-keepingAn append-only, hash-chained record for every enforced action, exportable on demand. Records cannot be edited or deleted once written.
OFAC SDN screeningEvery destination is screened before a decision; matches are denied.
Tamper evidenceSHA-256 hash chain; optional Solana devnet anchoring for an independent timestamp.
Data residencyAudit records are stored in the EU (Supabase, AWS eu-west-1, Ireland).
EU AI Act Art. 12 — loggingApplies 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 statusrule_triggered / codeMeaning
400input_validationMalformed JSON, invalid agent_slug, missing field, non-numeric amount, or unsupported currency.
401authMissing, invalid or revoked API key.
403auth · AUTH_INSUFFICIENT_SCOPE / AUTH_AGENT_MISMATCH / AUTH_AGENT_NOT_FOUNDKey lacks enforce scope, or the agent does not belong to the key's operator.
409policy_lookup · POLICY_NOT_FOUNDThe agent has no active policy. Set its limits in the dashboard.
429rate_limitMore than 300 requests per minute for one agent, or 100 per minute from one IP. See X-RateLimit-* and Retry-After headers.
500decision_capture / system_errorThe decision could not be recorded or an unexpected error occurred — failing closed.
503auth / policy_lookup · AUTH_BACKEND_UNAVAILABLE / POLICY_LOOKUP_FAILEDA backend is unavailable — failing closed. Retry later.
Error response shape (HTTP 429)
{
  "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.