Agent-to-Secure Payload Authorization
A2SPA is the cryptographic firewall for AI agents, ensuring that every autonomous action is signed, verified, authorized, and monitored. Build secure agent-to-agent communication with enforced permissions, replay protection, and comprehensive audit trails.
A2SPA (Agent-to-Secure Payload Authorization) is a secure protocol that enables verified, cryptographically signed payloads between AI agents. This platform allows developers and non-technical users to create, manage, and monitor modular AI agents, all protected by A2SPA.
/A2SPA/register/A2SPA/login/A2SPA/forgotIf you want browser AI assistants or coding agents to wire existing agents into A2SPA, use the public API Integration Pack. It teaches AI how to create agents through the dashboard Builder Pack and how to add runtime code that still routes through the A2SPA API instead of bypassing it.
POST /A2SPA/api/verify_payload.
POST /A2SPA/api/verify_payload for sending and
GET /A2SPA/api/inbox_for_agent for polling received
messages. Use GET /A2SPA/api/logs_for_agent for audit
history and troubleshooting.
Use the dashboard to create agents. Choose an agent name, decide whether it can send and/or receive, and keep the generated private key in a secure location. If you use a browser-based AI assistant, open the dashboard's Agent Builder Pack for the same flow in a machine-readable format.
{
"agent_name": "calendarAgent",
"permissions": {
"send": true,
"receive": false
},
"enabled": true
}
| Permission | Description |
|---|---|
send |
Can initiate actions or payloads |
receive |
Can be a target agent and receive payloads |
š Helps prevent abuse even if an agent is compromised.
To send a message (payload), you need to create it, hash it, sign it, and send it to A2SPA for verification. Here's how it works:
What You Need:
import json, uuid, requests, hashlib, re
from datetime import datetime, timezone
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
# ============ Settings ============
API_BASE = "https://aimodularity.com/A2SPA"
API_KEY = "YOUR_API_KEY_HERE" # Get from dashboard
USER_ID = "your_user_id"
SENDER_AGENT_ID = f"{USER_ID}_my_agent_send"
RECEIVER_AGENT_ID = f"{USER_ID}_my_agent_receive"
PRIVATE_KEY_PATH = "your_sender_agent.priv.pem"
SIGNABLE_FIELDS = (
"agent_id",
"target_agent_id",
"timestamp",
"nonce",
"input",
"output",
"alert_threshold",
)
EXPECTED_HASH_RE = re.compile(r"expected:\s*([0-9a-f]{64})", re.IGNORECASE)
# ============ Helper Function ============
def normalize_a2spa_payload(payload):
normalized = {field: payload[field] for field in SIGNABLE_FIELDS if field in payload}
normalized.setdefault("alert_threshold", 10)
return normalized
def canonicalize_a2spa_payload(payload):
return json.dumps(normalize_a2spa_payload(payload), sort_keys=True)
def prepare_signed_bytes(payload):
return canonicalize_a2spa_payload(payload).encode("utf-8")
def compute_payload_hash(payload):
return hashlib.sha256(prepare_signed_bytes(payload)).hexdigest()
def finalize_payload(payload_before_hash):
final_payload = {k: v for k, v in payload_before_hash.items() if k not in ("hash", "signature")}
final_payload["hash"] = compute_payload_hash(payload_before_hash)
return final_payload
def sign_payload(private_key, payload_before_hash):
return private_key.sign(
prepare_signed_bytes(payload_before_hash),
padding.PKCS1v15(),
hashes.SHA256(),
).hex()
def build_request_body(payload, signature):
return {"payload": payload, "signature": signature}
def extract_expected_hash(error_text):
match = EXPECTED_HASH_RE.search(str(error_text or ""))
return match.group(1) if match else None
# ============ Step 1: Create Message ============
payload_before_hash = {
"agent_id": SENDER_AGENT_ID,
"target_agent_id": RECEIVER_AGENT_ID,
"timestamp": datetime.now(timezone.utc).isoformat(),
"nonce": str(uuid.uuid4()), # Unique code to prevent reuse
"input": {"message": "Hello from sender agent!"},
"output": {"status": "ready"},
"alert_threshold": 10,
}
print("ā
Step 1: Message created")
# ============ Step 2: Hash the Message ============
payload = finalize_payload(payload_before_hash)
print(f"ā
Step 2: Hash created: {payload['hash'][:16]}...")
# ============ Step 3: Sign the Message ============
with open(PRIVATE_KEY_PATH, "rb") as f:
PRIVATE_KEY_PEM = f.read()
private_key = serialization.load_pem_private_key(
PRIVATE_KEY_PEM,
password=None
)
signature_hex = sign_payload(private_key, payload_before_hash)
print(f"ā
Step 3: Signature created: {signature_hex[:16]}...")
# ============ Step 4: Send to A2SPA ============
request_body = build_request_body(payload, signature_hex)
resp = requests.post(
f"{API_BASE}/api/verify_payload",
headers={
"Content-Type": "application/json",
"x-api-key": API_KEY
},
json=request_body
)
print(f"\nšÆ Result:")
print(f"Status Code: {resp.status_code}")
print(f"Response: {resp.text}")
if resp.status_code == 200:
print("\nā
Success! Message verified and logged.")
else:
debug_snapshot = {
"payload_before_hash": payload_before_hash,
"canonical_string": canonicalize_a2spa_payload(payload_before_hash),
"canonical_bytes_hex": prepare_signed_bytes(payload_before_hash).hex(),
"client_computed_hash": compute_payload_hash(payload_before_hash),
"final_request_body": request_body,
"server_expected_hash": extract_expected_hash(resp.text),
}
print("\nā Hash/signature debug:")
print(json.dumps(debug_snapshot, indent=2, sort_keys=True))
The generated JavaScript and TypeScript starters now build payload_before_hash, compute payload.hash from the verifier-normalized payload, sign the exact same canonical UTF-8 bytes, and then send { payload, signature }.
const input = { message: "Hello from my existing Node agent" };
const output = await existingAgent(input);
const signedRequest = await client.buildSignedRequest(
process.env.A2SPA_TARGET_AGENT_ID,
input,
output,
);
const firstResponse = await client.postRequestBody(signedRequest.requestBody);
console.log(`First request: ${firstResponse.status} ${await firstResponse.text()}`);
const secondResponse = await client.postRequestBody(signedRequest.requestBody);
console.log(`Second request: ${secondResponse.status} ${await secondResponse.text()}`);
// Expected live behavior:
// First request: 200 {"success":true}
// Second request: 409 {"error":"Replay attack detected - nonce already used"}
Avoid global string replacements such as .replace(/:/g, ": ") or .replace(/,/g, ", "). They corrupt timestamps and other string values instead of formatting only JSON separators.
2026-06-17T19:45:30.012Z into 2026-06-17T19: 45: 30.012Z. The helper must preserve string values exactly.
curl -H "x-api-key: YOUR_API_KEY_HERE" "https://aimodularity.com/A2SPA/api/inbox_for_agent?agent_id=YOUR_RECEIVER_AGENT_ID&limit=25"
Use the inbox endpoint when an existing agent needs to poll for received
messages through the A2SPA API. Use /api/logs_for_agent
when you want audit history or troubleshooting detail.
| Field | Required? | Description | Example |
|---|---|---|---|
| agent_id | Yes | Sender's agent ID | abc123_myAgent |
| target_agent_id | Yes | Receiver's agent ID | def456_otherAgent |
| timestamp | Yes | UTC time of message | 2025-10-07T14:30:00Z |
| nonce | Yes | Unique code to prevent reuse | a1b2c3d4-e5f6-... |
| input | Yes | Data sent to agent | {"task": "process"} |
| output | Yes | Agent's response | {"result": "done"} |
| alert_threshold | No | Optional low-credit warning threshold; defaults to 10 before hashing/signing | 10 |
| hash | Yes | Message fingerprint | a3f8e2b9c1d... |
The verifier hashes and signs the schema-normalized payload, not the raw request body. The current contract matches Python json.dumps(payload, sort_keys=True) over the signable payload fields.
| Rule | Current verifier contract |
|---|---|
| Recursive key sorting | Object keys are sorted at every nested level before hashing or signing. |
| UTF-8 bytes | The canonical JSON string is encoded as UTF-8 before SHA-256 or RSA signing. |
| Array order | Array order is preserved exactly as provided. |
| Whitespace | Use Python default separators: comma + space and colon + space. |
| String escaping | Use Python json.dumps(..., sort_keys=True) escaping semantics, including ensure_ascii=True behavior for non-ASCII characters. |
| Number handling | Use finite JSON numbers only. If exact decimal spelling matters across languages, send it as a string because JavaScript cannot distinguish 1 from 1.0 after parsing. |
| Timestamp format | Send an ISO-8601 UTC string such as 2026-06-17T12:34:56Z or 2026-06-17T12:34:56+00:00. Current freshness enforcement is 120 seconds. |
| Hash algorithm | Lowercase hex SHA-256 digest. |
| Signed byte range | Sign the canonical UTF-8 bytes of the same signable payload used for hashing. |
| Fields included in hash | agent_id, target_agent_id, timestamp, nonce, input, output, and alert_threshold. |
| Fields excluded from hash | hash, signature, and extra metadata not in the current verifier schema. |
When to compute payload.hash | Build the signable payload first, compute the hash before adding the hash field, then sign the same canonical bytes. |
| Error | Cause | Fix |
|---|---|---|
| Signature verification failed | Wrong private key or different canonical bytes were signed | Compute the hash from the signable payload, then sign those exact same canonical bytes with the correct sender key. |
| Agent not found | Wrong agent ID | Copy ID exactly from dashboard. |
| Timestamp too old | Message older than 2 minutes | Send immediately using datetime.now(timezone.utc). |
| Replay attack detected | Reused nonce | Use a new str(uuid.uuid4()). |
POST /A2SPA/api/verify_payload: 120 requests per minute per client IP by defaultGET /A2SPA/api/inbox_for_agent: 60 requests per minute per client IP by defaultGET /A2SPA/api/logs_for_agent: 60 requests per minute per client IP by defaultPOST /A2SPA/login: 10 requests per 15 minutes per IPPOST /A2SPA/register: 5 requests per hour per IPPOST /A2SPA/forgot: 5 requests per hour per IPPOST /A2SPA/billing/topup: 10 requests per 15 minutes per signed-in userHTTP 429
plus a Retry-After header. This helps protect the
platform against abuse while keeping dashboard and API usage
predictable for customers.
/A2SPA/api/toggle_agent_status
| Mistake | Fix |
|---|---|
| Reusing a nonce or timestamp | Use uuid4() and datetime.utcnow() |
| Sending wrong agent_id | Match dashboard agent exactly |
| Forgetting to hash the payload | Call compute_payload_hash() |
| Signing different bytes than the hash | Compute the hash from the signable payload, then sign those exact same canonical bytes |
| Payload too old (>2 min) | Send immediately, use NTP-synced clocks |
| Agent is toggled OFF | Go to dashboard and toggle it ON |
| Term | What It Means | Why It Matters |
|---|---|---|
| Nonce | One-time string (UUID) | Stops replay attacks |
| Hash | Fingerprint of the payload | Proves nothing changed |
| Signature | Cryptographic proof created with the sender private key and verified with the sender public key | Proves you sent it |
| API Key | Your account-level API token for calling A2SPA endpoints | Authenticates API requests and tracks usage |
on_behalf_of is never authorization by itself. It is metadata unless backed by a verified delegation proof.
It is not trusted unless backed by a verified delegation proof.
Cryptographic binding proves integrity. Policy determines semantic permission.
No signature. No execution.
The A2SPA dashboard provides comprehensive monitoring and management capabilities for your agents:
Each log entry contains:
| Status | Balance | Indicator | API Access |
|---|---|---|---|
| Normal | >10 tokens | Green status | Full access |
| Warning | <10 tokens | ā ļø Yellow banner | Full access |
| Critical | 0 tokens | š“ Red banner | API disabled |
A2SPA uses a simple, transparent pay-as-you-go model:
Verification logs can include ROI values from an agent's stored ROI profile. If no ROI profile is stored, A2SPA uses a default fallback.
{
"roi": {
"time_saved_minutes": 3,
"value_usd": 0.08
}
}
Default ROI fallback if not provided:
{
"time_saved_minutes": 2,
"value_usd": 0.05
}
send permission and the target has
receive permission. Delivery threading is added to the
dashboard automatically.