Build with A2SPA
Everything you need to put an approval check in front of your agents' actions: quickstart, API reference, SDKs and the rules behind every decision.
No sections match your search.
Overview
A2SPA checks every action an AI agent wants to take before your systems run it. Your agent describes the action and signs it; A2SPA checks who sent it, that it wasn't changed, that it's current and that your rules allow it, then returns a signed decision. Your system acts only when the decision is authorized.
A2SPA doesn't build or run agents. You create a wrapper for each agent you already run. The wrapper holds the agent's signing key and rules.
Quickstart
- Create an account and open the dashboard.
- Create a wrapper for your agent (Dashboard → Wrappers → Create wrapper). Save the private key it shows; it's shown only once. If your agent sends actions to another system, create a wrapper for that system too and turn on Receive actions.
- Copy your API key (Dashboard → API key & setup). Store both secrets in a secret manager.
- Install the SDK:
pip install a2spa(Python 3.11–3.13) ornpm install a2spa(Node.js 22+). - Check each action before it runs:
import os, requests from a2spa import (build_payload_fields, build_signed_request, policy_input, require_authorized_result) API = "https://api.aimodularity.com/A2SPA" action = build_payload_fields( agent_id=os.environ["A2SPA_WRAPPER_ID"], target_agent_id=os.environ["A2SPA_TARGET_ID"], input_data=policy_input(action="export_customer_records", workflow_scope="privacy:export", customer_id="10442"), ) body = build_signed_request(os.environ["A2SPA_PRIVATE_KEY"], action) result = requests.post(f"{API}/v1/verify_payload", json=body, timeout=10, headers={"x-api-key": os.environ["A2SPA_API_KEY"]}).json() require_authorized_result(result) # raises A2SPAAuthorizationError unless authorized export_customer("10442") # only reached when approved
const a2spa = require("a2spa"); const API = "https://api.aimodularity.com/A2SPA"; const action = a2spa.buildPayloadFields({ agentId: process.env.A2SPA_WRAPPER_ID, targetAgentId: process.env.A2SPA_TARGET_ID, input: a2spa.policyInput({ action: "export_customer_records", workflowScope: "privacy:export", customer_id: "10442" }), }); const body = a2spa.buildSignedRequest(action, process.env.A2SPA_PRIVATE_KEY); const res = await fetch(`${API}/v1/verify_payload`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.A2SPA_API_KEY }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000), }); a2spa.requireAuthorizedResult(await res.json()); // throws unless authorized await exportCustomer("10442"); // only reached when approved
That's it. The decision appears in Dashboard → Activity, with its signed receipt.
Concepts
- Wrapper
- Connects one agent you run to A2SPA. It has an ID, a signing key, permissions (can it request actions, can it receive them) and rules. Wrappers are created in the dashboard; the private key is shown once.
- Action
- What the agent wants to do, described as a signed JSON payload: who it's from, who it's for, the details, a timestamp and a one-time value.
- Decision
authorized,refer_hold(held for review) ordenied. Onlyauthorizedmeans "go". A request that fails a check before a decision is reached gets an error instead.- Receipt
- A record of the decision, signed by A2SPA (RSA-PSS-SHA256), that you can store and verify later.
- Rules
- Per-wrapper policy: which actions, scopes, targets, currencies and amounts are allowed.
Authentication & base URL
| Base URL | https://api.aimodularity.com/A2SPA |
|---|---|
| Auth | Header x-api-key: <your API key> on every request |
| Format | JSON, Content-Type: application/json |
| Versioning | Canonical routes are under /v1. Older /api/* routes remain as aliases during the migration window. |
| Contract | OpenAPI 3.1 specification |
The API key identifies your account; the wrapper's private key proves each action. Both must stay secret. Never ship either in client-side code.
The signed action
Every check sends {"payload": {...}, "signature": "<hex>"}. The SDK's build_payload_fields and build_signed_request fill this in for you.
| Field | Required | Description |
|---|---|---|
agent_id | Yes | The sending wrapper's ID. |
target_agent_id | Yes | The receiving wrapper's ID. |
timestamp | Yes | ISO 8601 time the action was signed. Must be within 120 seconds of A2SPA's clock. |
nonce | Yes | One-time value, 8–512 characters (A-Z a-z 0-9 _ . : -). Never reuse it. |
input | Yes | The action details (object or string). Put the facts your rules check here: action, workflow_scope, amount_usd, currency, on_behalf_of, plus your own fields. |
output | Yes | Expected result or status (object or string). |
hash | Yes | SHA-256 (hex) of the signed bytes. See Signing. |
alert_threshold | No | Low-balance warning threshold, 0–1,000,000. Default 10. |
crypto_profile | No | Signature profile. Default a2spa.classical.rsa_pkcs1v15_sha256.v1. |
operation_id | No | Your idempotency key (up to 160 characters) for safe retries. |
state_continuity | No | Ties the action to registered state. See continuity_state. |
delegation | No | Signed delegation proof when acting on someone else's behalf. |
Signing
The SDKs handle signing. If you implement it yourself, the signed bytes are UTF-8 JSON of the signable fields with keys sorted by Unicode code point, Python-style separators (", " and ": ") and non-ASCII characters escaped as \uXXXX.
- Signable fields:
agent_id,target_agent_id,timestamp,nonce,input,output,alert_threshold(default 10), and, when present,crypto_profile,state_continuity,operation_id,delegation. - hash = hex SHA-256 of the signed bytes.
- signature = hex signature of the same bytes with the wrapper's private key.
- Numbers: the JavaScript SDK accepts safe integers only. Send decimals as strings, for example
"amount_usd": "49.00".
| Profile | Algorithm |
|---|---|
a2spa.classical.rsa_pkcs1v15_sha256.v1 | RSA PKCS#1 v1.5, SHA-256 (default) |
a2spa.pq.mldsa.v1 | ML-DSA post-quantum signatures (optional) |
a2spa.legacy.rsa_pkcs1v15_sha256.v1 | Legacy compatibility |
POST /v1/verify_payload
Checks a signed action and returns a decision. Never infer approval from the HTTP status alone. Use require_authorized_result / requireAuthorizedResult, which pass only an explicit authorized decision.
// 200: approved { "success": true, "receipt": { "receipt": { "receipt_id": "rct_...", "decision": "authorized", "agent_id": "...", "target_agent_id": "...", "issued_at": "..." }, "canonical_hash": "...", "signature": "...", "signature_algorithm": "RSA-PSS-SHA256", "key_id": "..." } }
// 403: denied before a decision { "error": "Policy denied - workflow scope not allowed" } // 409: one-time value reused { "error": "Replay attack detected - nonce already used" } // 429: slow down { "error": "Rate limit exceeded", "retry_after": 12 }
| Status | Meaning |
|---|---|
| 200 | A decision was made. It may be authorized, refer_hold or denied: check it. |
| 400 | Malformed request, bad hash or signature, or timestamp outside 120 seconds. |
| 401 | Missing or invalid API key. |
| 403 | Not allowed: rules, permissions, an inactive wrapper or target, delegation, or insufficient credits. |
| 409 | One-time value reused with different content, an operation still in progress, or a state conflict. |
| 429 | Rate limited. Wait retry_after seconds. |
| 500 / 503 | Temporary server problem. Treat as "do not run". |
Decisions & receipts
| Decision | Your system should |
|---|---|
| authorized | Run the action. Store the receipt ID with it. |
| refer_hold | Don't run it. Route it to a person for review. |
| denied | Don't run it. |
| Error / timeout | Don't run it. Retry safely (see Errors). |
GET /v1/receipts/{receipt_id}
Fetch a receipt your account owns, for example during an audit. Receipts are signed with RSA-PSS-SHA256; the Python SDK's verify_execution_receipt(receipt, trusted_public_key_pem) checks one offline against A2SPA's receipt public key (available on request).
curl -H "x-api-key: $A2SPA_API_KEY" \
https://api.aimodularity.com/A2SPA/v1/receipts/rct_...
POST /v1/continuity_state
Register a fact that must still be true when an action runs, such as money reserved, stock available or an approval still valid. Actions that reference the state in state_continuity are approved only while it matches, and each sequence can be used once.
| Field | Required | Description |
|---|---|---|
state_id | Yes | Your ID for the state (up to 160 characters). |
sequence | Yes | Integer ≥ 0. Increase it when the state changes. |
binding | Yes | Which sender and target may use this state. |
state or state_hash | One of | The state JSON, or its SHA-256. |
status | No | active (default) or revoked. |
expires_at | No | ISO 8601 expiry. |
Inbox & logs
GET /v1/inbox_for_agent?agent_id=…&after=…&limit=…
Approved actions delivered to a receiving wrapper, oldest first. Poll with after set to the timestamp of the last message you processed; limit is 1–100 (default 50). Keep polling until you get an empty page.
GET /v1/logs_for_agent?agent_id=…
Recent decisions for audit and troubleshooting. Use the inbox, not logs, as your runtime delivery feed.
Errors & retries
- Fail closed. Anything other than an explicit authorized decision means the action doesn't run, including timeouts.
- Safe retries. Re-sending the exact same signed request returns the original decision and receipt; nothing is approved twice. Use
operation_idfor retries that rebuild the request. - Never reuse a one-time value with different content: that's treated as a replay (409).
- Rate limits apply per API key and per IP. On 429, wait
retry_afterseconds. - Clock skew. Keep servers on NTP; timestamps more than 120 seconds off are refused.
Rule (policy) reason codes
When rules deny an action, these stable codes may appear in decisions and evidence:
POLICY_TARGET_MISMATCHPOLICY_ACTION_MISMATCHPOLICY_RESOURCE_MISMATCHPOLICY_PARAMETER_MISMATCHPOLICY_PAYLOAD_MISMATCHPOLICY_WORKFLOW_MISMATCHPOLICY_ENVIRONMENT_MISMATCHPOLICY_USAGE_EXHAUSTEDPOLICY_APPROVAL_REQUIREDPOLICY_UNSUPPORTED_REQUIRED_CONSTRAINTPOLICY_INDETERMINATE
Wrapper rules
Set rules per wrapper in Dashboard → Wrappers → Rules. Start from a preset (Basic, Scoped, Restricted, Exact action) or edit JSON. Rules compare the facts in the signed input against what's allowed.
{
"allowed_actions": ["refund", "credit_note"],
"allowed_workflow_scopes": ["billing:refunds"],
"allowed_currencies": ["USD"],
"max_spend_usd": 500
}
| Rule | Checks |
|---|---|
allowed_actions | The action fact. |
allowed_workflow_scopes | The workflow_scope fact. |
allowed_targets / allowed_senders | Who a wrapper may send to, or accept from. |
allowed_intents | The intent fact. |
allowed_currencies, max_spend_usd | The currency and amount_usd facts. |
policy_constraints | Exact-action constraints: resource, environment, parameters, payload hash, usage limits. |
delegation | Whether, and for whom, the wrapper may act on someone's behalf. |
SDKs
Python
pip install a2spa · Python 3.11–3.13
build_payload_fields, build_signed_request, policy_input, require_authorized_result, authorization_decision, verify_execution_receipt, execute_protected_action
Node.js
npm install a2spa · Node.js 22+
buildPayloadFields, buildSignedRequest, policyInput, requireAuthorizedResult, authorizationDecision, computePayloadHash
For step-by-step code, environment variables and templates, see the integration guide.
AI coding assistants
Give your coding assistant this prompt to add A2SPA to an existing repository:
Add A2SPA authorization to this repository using the published a2spa package and the A2SPA integration guide (https://aimodularity.com/A2SPA/integrations). - Every consequential action (payments, data exports, writes, deploys) must be signed and sent to POST /v1/verify_payload before it runs. - Read the wrapper ID, private key and API key from environment variables or the secret manager. Never hard-code them. - Proceed only when require_authorized_result passes. Treat errors, timeouts, refer_hold and denied as "do not run". - Put the facts the rules check (action, workflow_scope, amount_usd, currency) in the signed input. - Do not bypass the A2SPA API or write to its storage directly.
Security model
- Each wrapper signs its own actions; A2SPA never holds your agents' private keys after issuing them.
- Approvals bind the exact action: changing any signed field invalidates the signature.
- Each approval is single-use and time-limited.
- Decisions are signed receipts, verifiable independently.
- Put the check where the action happens. A check beside the path can be walked around.
- A2SPA doesn't judge whether an action is wise. Loose rules approve what they allow, and a stolen wrapper key can sign until you revoke it.
Report security issues to hello@aiblockchainventures.com.
Versions
Hosted service and SDKs share a 2.4.x version line on protocol v1.0. Patch releases keep the v1 API contract unchanged. The current service reports its version in the X-A2SPA-Version response header.