Build agents behind an authorization gate.
These docs are written for both humans and AI coding assistants. The goal is simple: create agents, store keys safely, sign each consequential action, submit to A2SPA, and proceed only after an authorized receipt is returned.
Showing all sections.
Fastest working path
- Create an account. New accounts may include starter credits when public registration is enabled.
- Create two agents. One sender and one receiver. Download the key bundle immediately.
- Store private keys outside chat. Put private keys in a secret manager, deployment secret, or local file outside source control referenced by an environment variable.
- Wrap the existing agent. Build and sign a payload before the agent calls a consequential tool.
- Call
POST /api/verify_payload. Only proceed if A2SPA returnssuccess: trueand a receipt decision ofauthorized.
Tell an AI coding assistant this
Use the A2SPA Integration Pack.
Create or identify sender and receiver agents.
Store the issued private key in my secret store.
Wrap every consequential action so it builds a signed A2SPA payload.
Submit the signed request to POST /api/verify_payload.
Do not proceed unless A2SPA returns success: true and receipt.receipt.decision == "authorized".
Do not write directly to backend storage.
Do not paste private keys into chat.
How an AI can build this for a customer
The assistant should use the authenticated dashboard for agent setup and the Integration Pack for code generation inside the user's repo.
Dashboard Setup
Used from a signed-in browser session to create agents, manage keys, set policy, and download issued key bundles.
Integration Pack
Used in the customer's codebase to add signing helpers, environment variables, runtime wrapper functions, and polling helpers.
Assistant operating rules
- Ask what actions the user's agents perform: email, calendar, payment, database write, code deployment, browser action, or custom workflow.
- Create one A2SPA agent per runtime identity, not one giant shared agent for everything.
- Map each action to signed
input.action,input.workflow_scope, optionalamount_usd,currency, andon_behalf_of. - Set dashboard policy so the action cannot exceed its intended scope.
- For money, inventory, approvals, or long-running workflows, register state continuity before continuation.
Identity answers who. A2SPA checks what may proceed.
A2SPA should sit immediately before a consequential action. If signature, nonce, timestamp, ownership, policy, or continuity checks fail, the action should not proceed. A2SPA receipts prove verifier decisions, not target-side commit unless your integration adds target acknowledgements.
Each runtime identity gets an agent key.
A2SPA uses an account API key to authenticate calls and per-agent private keys to sign payloads. Private keys are shown only when issued or rotated.
| Item | Purpose | Where It Lives |
|---|---|---|
| Account API key | Authenticates requests to A2SPA APIs. | Server secret or environment variable. |
| Agent private key | Signs the exact action payload. | Your secret manager. Never chat. |
| Agent public key | Used by A2SPA verifier to check signatures. | Stored by A2SPA. |
| Key bundle | One JSON file containing issued key material. | Downloaded once and stored securely. |
The helper libraries are narrow by design.
A2SPA payload helpers build canonical payloads, compute hashes, sign actions, build policy facts, and create state-continuity claims. They do not verify or enforce. Enforcement stays on A2SPA server APIs.
Python package target
pip install a2spa
from a2spa import build_payload_fields, build_signed_requestNode package target
npm install a2spa
const { buildPayloadFields, buildSignedRequest } = require("a2spa");Distributed helper libraries are client-side helpers only. A2SPA server APIs remain the enforcement authority.
Copy-paste code examples
Run these examples in server-side code. Keep A2SPA_API_KEY and the agent private key in environment variables or a secret manager, never in browser code.
Python: sign, verify authorization, then proceed
import json
import os
from pathlib import Path
from urllib import error, request
from a2spa import build_payload_fields, build_signed_request, policy_input, require_authorized_result
base_url = os.getenv("A2SPA_API_BASE", "https://api.aimodularity.com/A2SPA").rstrip("/")
api_key = os.environ["A2SPA_API_KEY"]
private_key_text = Path(os.environ["A2SPA_PRIVATE_KEY_PATH"]).read_text(encoding="utf-8")
payload = build_payload_fields(
agent_id=os.environ["A2SPA_AGENT_ID"],
target_agent_id=os.environ["A2SPA_TARGET_AGENT_ID"],
input_data=policy_input(action="send_email", workflow_scope="email:send"),
output_data={"status": "ready"},
)
request_body = build_signed_request(private_key_text, payload)
http_request = request.Request(
f"{base_url}/api/verify_payload",
data=json.dumps(request_body).encode("utf-8"),
headers={"Content-Type": "application/json", "x-api-key": api_key},
method="POST",
)
try:
with request.urlopen(http_request, timeout=15) as response:
result = json.loads(response.read().decode("utf-8"))
except error.HTTPError as exc:
result = json.loads(exc.read().decode("utf-8") or "{}")
raise RuntimeError(result.get("error", f"A2SPA denied the action ({exc.code})")) from exc
require_authorized_result(result)
receipt_id = result["receipt"]["receipt"]["receipt_id"]
print(f"A2SPA authorized the action. Receipt: {receipt_id}")
# Caller-side continuation may proceed only after this point.
# Target commit and exactly-once effects still depend on integration idempotency.
Node: sign, verify authorization, then proceed
const fs = require("node:fs");
const { buildPayloadFields, buildSignedRequest, policyInput, requireAuthorizedResult } = require("a2spa");
async function main() {
const baseUrl = (process.env.A2SPA_API_BASE || "https://api.aimodularity.com/A2SPA").replace(/\/$/, "");
const privateKeyPem = fs.readFileSync(process.env.A2SPA_PRIVATE_KEY_PATH, "utf8");
const payload = buildPayloadFields({
agentId: process.env.A2SPA_AGENT_ID,
targetAgentId: process.env.A2SPA_TARGET_AGENT_ID,
input: policyInput({ action: "send_email", workflowScope: "email:send" }),
output: { status: "ready" },
});
const requestBody = buildSignedRequest(payload, privateKeyPem);
const response = await fetch(`${baseUrl}/api/verify_payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.A2SPA_API_KEY,
},
body: JSON.stringify(requestBody),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.reason_code || result.error || `A2SPA did not authorize delivery (${response.status})`);
}
requireAuthorizedResult(result);
const receiptId = result.receipt.receipt.receipt_id;
console.log(`A2SPA authorized the action. Receipt: ${receiptId}`);
// Caller-side continuation may proceed only after this point.
// Target commit and exactly-once effects still depend on integration idempotency.
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
Signed request shape
{
"payload": {
"agent_id": "user-123_sender",
"target_agent_id": "user-123_receiver",
"timestamp": "2026-08-10T20:00:00Z",
"nonce": "unique-per-action",
"input": {"action": "send_email", "workflow_scope": "email:send"},
"output": {"status": "ready"},
"alert_threshold": 10,
"crypto_profile": "a2spa.classical.rsa_pkcs1v15_sha256.v1",
"hash": "client-computed-sha256"
},
"signature": "hex-encoded-agent-signature"
}Authorized response shape
{
"success": true,
"receipt": {
"receipt": {
"receipt_id": "rct_example",
"decision": "authorized"
},
"canonical_hash": "receipt-canonical-sha256"
}
}Blocked governance response shape
{
"success": false,
"decision": "refer_hold",
"delivery": {"status": "blocked"},
"reason_code": "AUTHORITY_CONTINUITY_UNRESOLVED",
"receipt": {
"receipt": {
"receipt_id": "rct_example",
"decision": "refer_hold"
}
}
}Receipt lookup
curl https://api.aimodularity.com/A2SPA/api/receipts/$RECEIPT_ID \
-H "x-api-key: $A2SPA_API_KEY"
What gets signed
The public term is signature profile. The stable wire field remains crypto_profile.
{
"agent_id": "sender-agent-id",
"target_agent_id": "receiver-agent-id",
"timestamp": "2026-08-10T20:00:00Z",
"nonce": "unique-per-action",
"input": {
"action": "reserve",
"workflow_scope": "payments:reserve",
"amount_usd": "100.00",
"currency": "USD"
},
"output": {"status": "ready"},
"alert_threshold": 10,
"crypto_profile": "a2spa.classical.rsa_pkcs1v15_sha256.v1"
}
Hash exactly what A2SPA verifies.
A2SPA signs a stable subset of the payload. Clients should build the payload, compute the hash over the canonical signable fields, then sign that same pre-hash payload. Do not hash transport wrappers, HTTP headers, or local debug fields.
| Included Field | Why It Matters |
|---|---|
agent_id | Binds the sender runtime identity. |
target_agent_id | Binds the intended receiver or execution boundary. |
timestamp | Prevents stale payloads from being accepted outside the verifier window. |
nonce | Supports one-time replay protection. |
input and output | Bind the action facts and expected downstream result. |
alert_threshold | Preserves client-side low-balance warning intent. |
crypto_profile | Selects the signature profile; required for non-legacy profiles and included by current helpers. |
state_continuity | Optional one-time continuation claim. |
Use payload helpers unless you are intentionally implementing the canonicalization contract yourself.
Signed Bytes V1 Compatibility Profile
signable = {
agent_id,
target_agent_id,
timestamp,
nonce,
input,
output,
alert_threshold defaulting to 10,
crypto_profile when present,
state_continuity when present
}
canonical_string = Python json.dumps(signable, sort_keys=True)
signed_bytes = canonical_string encoded as UTF-8
hash = SHA-256(signed_bytes)
signature = crypto_profile algorithm over signed_bytes
hash and signature fields are excluded from signed_bytes.
Current A2SPA 2.0 helper compatibility uses Python-compatible JSON bytes. Do not substitute a different JSON canonicalization rule unless A2SPA publishes a new crypto profile for it.
POST /api/verify_payload
Call this before downstream delivery or caller-side continuation proceeds. A2SPA checks API key ownership, sender key, target agent, signature profile, hash, signature, timestamp, nonce, policy, credits, and optional state continuity.
curl -X POST https://api.aimodularity.com/A2SPA/api/verify_payload \
-H "Content-Type: application/json" \
-H "x-api-key: $A2SPA_API_KEY" \
-d @signed-request.json
Policy tells A2SPA what an agent may do.
Put policy-relevant facts in signed input. Then set a policy that allows only those facts. If a signed payload asks for an unapproved action, scope, spend amount, currency, or delegation principal, A2SPA denies before nonce consumption.
Email Agent
{
"allowed_actions": ["send_email", "draft_email"],
"allowed_workflow_scopes": ["email:send", "email:draft"]
}Payment Reservation Agent
{
"allowed_actions": ["reserve"],
"allowed_intents": ["payment_reservation"],
"allowed_workflow_scopes": ["payments:reserve"],
"allowed_currencies": ["USD"],
"max_spend_usd": 100,
"require_state_continuity_for_spend": true
}Use policy as an execution contract, not a label.
Strict policies should allow only the facts the agent is expected to sign. For consequential actions, require action, workflow scope, amount, currency, and state continuity where appropriate.
Outbound Sender Policy
Limit what the sender may request: allowed targets, actions, workflow scopes, spend ceilings, currencies, and delegation principals.
Inbound Receiver Policy
Limit what the receiver will accept: allowed senders, accepted actions, inbound spend ceilings, accepted scopes, and approved intents.
{
"allowed_targets": ["user-123_fulfillment-agent"],
"allowed_actions": ["reserve"],
"allowed_intents": ["payment_reservation"],
"allowed_workflow_scopes": ["payments:reserve"],
"allowed_currencies": ["USD"],
"max_spend_usd": 100,
"require_state_continuity_for_spend": true,
"delegation": {
"enabled": true,
"allowed_principals": ["finance-team"]
}
}
Lock the truth a workflow depends on.
Use state continuity when an agent action depends on something still being true: money still reserved, inventory still available, approval still valid, a ticket still open, or a workflow step not expired. A2SPA 2.0 state-continuity records bind state to the sender, receiver, action, workflow scope, and optional spend facts by default.
POST /api/continuity_state
x-api-key: $A2SPA_API_KEY
{
"state_id": "payment-123-reservation",
"state": {"reserved_amount": "100.00", "currency": "USD", "status": "reserved"},
"binding": {
"agent_id": "user-123_payment-agent",
"target_agent_id": "user-123_fulfillment-agent",
"action": "reserve",
"workflow_scope": "payments:reserve",
"amount_usd": "100.00",
"currency": "USD"
},
"sequence": 1,
"status": "active",
"expires_at": "2026-08-10T20:00:00Z"
}
A verified sequence is one-time. Register the next sequence before the next continuation step.
Proof for processed decisions
Processed requests can produce and store an authorization/delivery receipt containing sender, target, payload hash, nonce, timestamp, verifier metadata, policy checks, signature profile, continuity checks, delivery status, and a canonical receipt hash. A receipt decision may be authorized, refer_hold, or denied; downstream delivery should only occur when the decision is explicitly authorized. This receipt is not proof of target-side execution or commit unless your integration adds target acknowledgement semantics.
Retrieve stored receipts with GET /api/receipts/<receipt_id> from an authenticated session or with the account API key.
curl https://api.aimodularity.com/A2SPA/api/receipts/$RECEIPT_ID \
-H "x-api-key: $A2SPA_API_KEY"
Operational control plane
- Create agents, enable/disable them, rotate keys, revoke keys, and delete stale agents.
- Reveal/copy only newly issued API keys. Stored keys are hashed, so rotate to issue a visible key.
- Set policy using presets or JSON.
- Register state continuity records for workflows that need continuation checks.
- Review verified logs, expand payload details, export CSV, and inspect error logs.
Common denials
| Reason | Fix |
|---|---|
| Missing API key | Send x-api-key with the account API key. |
| Hash mismatch | Use helper canonicalization and sign before adding hash. |
| Nonce replay | Generate a fresh nonce for every action. |
| Policy denied | Confirm signed input facts match the sender and receiver policy. |
| State continuity failed | Register current state first and include a matching claim. |
| Insufficient credits | Add credits in the dashboard billing section. |