Developer Docs

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.

Start Here

Fastest working path

  1. Create an account. New accounts may include starter credits when public registration is enabled.
  2. Create two agents. One sender and one receiver. Download the key bundle immediately.
  3. 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.
  4. Wrap the existing agent. Build and sign a payload before the agent calls a consequential tool.
  5. Call POST /api/verify_payload. Only proceed if A2SPA returns success: true and a receipt decision of authorized.

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.
AI Assistants

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, optional amount_usd, currency, and on_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.
Mental Model

Identity answers who. A2SPA checks what may proceed.

Identity
Governance
Policy
A2SPA Check
Authorized Continuation

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.

Agents And Keys

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.

ItemPurposeWhere It Lives
Account API keyAuthenticates requests to A2SPA APIs.Server secret or environment variable.
Agent private keySigns the exact action payload.Your secret manager. Never chat.
Agent public keyUsed by A2SPA verifier to check signatures.Stored by A2SPA.
Key bundleOne JSON file containing issued key material.Downloaded once and stored securely.
Payload Helpers

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_request

Node 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.

Code Examples

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"
Payload Envelope

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"
}
Canonicalization

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 FieldWhy It Matters
agent_idBinds the sender runtime identity.
target_agent_idBinds the intended receiver or execution boundary.
timestampPrevents stale payloads from being accepted outside the verifier window.
nonceSupports one-time replay protection.
input and outputBind the action facts and expected downstream result.
alert_thresholdPreserves client-side low-balance warning intent.
crypto_profileSelects the signature profile; required for non-legacy profiles and included by current helpers.
state_continuityOptional 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.

Verify API

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 And Scopes

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
}
Advanced Policy

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"]
  }
}
State Continuity

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.

Register State
Sign Action
Verify
Check State
Continue Or Deny
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.

Receipts

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"
Dashboard

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.
Errors

Common denials

ReasonFix
Missing API keySend x-api-key with the account API key.
Hash mismatchUse helper canonicalization and sign before adding hash.
Nonce replayGenerate a fresh nonce for every action.
Policy deniedConfirm signed input facts match the sender and receiver policy.
State continuity failedRegister current state first and include a matching claim.
Insufficient creditsAdd credits in the dashboard billing section.