This pack is for API-based integration only. It tells AI coding agents how to provision A2SPA agents through the dashboard Builder Pack and how to wrap existing Python, JavaScript, and TypeScript agents so they send signed payloads through the A2SPA API instead of bypassing the platform.
POST /api/verify_payload,
or replaces server-side verification with local-only checks.
If the AI has access to a logged-in browser session, it should use the Builder Pack to create agents, rotate keys, revoke keys, and capture the private key the moment it is issued.
The starter kits expose helpers for building canonical payloads, signing them locally, calling the A2SPA API, and polling a clean inbox endpoint for received messages.
A2SPA remains the verification authority. The local code signs payloads, but the platform still enforces timestamp, nonce, signature, permission, toggle, and credit checks through the API.
If a browser session is available, open the Builder Pack to create or rotate the needed agents first.
Save the returned private key immediately; it is only shown once.
Add the A2SPA environment variables to the target project.
Install one of the starter templates and wrap the existing agent with the provided send and fetch helpers.
Use the inbox_for_agent endpoint to poll received messages for the receiving agent.
Always send signed payloads through POST /api/verify_payload.
Always fetch received runtime messages through GET /api/inbox_for_agent.
Use GET /api/logs_for_agent for audit and troubleshooting, not as the main runtime inbox.
Do not verify payloads locally as a substitute for the API.
Do not read or write Firestore directly from customer agent code.
Do not bypass nonce, timestamp, signature, permission, or toggle checks.
Store the agent private key outside source control and outside public directories.
POST https://aimodularity.com/A2SPA/api/verify_payload
Submit signed payloads through this endpoint so A2SPA can enforce verification, permission checks, replay protection, and credit usage.
GET https://aimodularity.com/A2SPA/api/inbox_for_agent
Poll received messages for a target agent through the A2SPA API. Use this as the runtime receive path for existing agents.
GET https://aimodularity.com/A2SPA/api/logs_for_agent
Use runtime logs for audit history, debugging, and dashboard-style visibility. They are not the primary inbox for receiving agents.
These templates are designed for AI coding agents. They all use the
A2SPA API and all expose a run_and_send or
runAndSend helper so an existing agent can be wrapped with
minimal code changes.
A2SPA_API_BASE=https://aimodularity.com/A2SPA
A2SPA_API_KEY=YOUR_API_KEY_HERE
A2SPA_AGENT_ID=your_user_id_my_agent_send
A2SPA_PRIVATE_KEY_PATH=your_sender_agent.priv.pem
A2SPA_TARGET_AGENT_ID=your_user_id_my_agent_receive
import hashlib
import json
import os
import re
import uuid
from datetime import datetime, timezone
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
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)
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 compute_payload_hash(payload):
return hashlib.sha256(canonicalize_a2spa_payload(payload).encode("utf-8")).hexdigest()
def prepare_signed_bytes(payload):
return canonicalize_a2spa_payload(payload).encode("utf-8")
def finalize_payload(payload_before_hash):
finalized = {k: v for k, v in payload_before_hash.items() if k not in ("hash", "signature")}
finalized["hash"] = compute_payload_hash(payload_before_hash)
return finalized
def build_request_body(payload, signature):
return {"payload": payload, "signature": signature}
def extract_expected_hash(error_text):
if not error_text:
return None
match = EXPECTED_HASH_RE.search(str(error_text))
return match.group(1) if match else None
def build_debug_snapshot(payload_before_hash, signature=None, final_payload=None, server_error_text=None):
canonical_string = canonicalize_a2spa_payload(payload_before_hash)
canonical_bytes = prepare_signed_bytes(payload_before_hash)
request_payload = final_payload or finalize_payload(payload_before_hash)
snapshot = {
"payload_before_hash": payload_before_hash,
"canonical_string": canonical_string,
"canonical_bytes_hex": canonical_bytes.hex(),
"client_computed_hash": compute_payload_hash(payload_before_hash),
"final_request_body": build_request_body(request_payload, signature) if signature else {"payload": request_payload},
"server_expected_hash": extract_expected_hash(server_error_text),
}
if server_error_text is not None:
snapshot["server_error"] = server_error_text
return snapshot
def sign_payload(private_key, payload):
return private_key.sign(
prepare_signed_bytes(payload),
padding.PKCS1v15(),
hashes.SHA256(),
).hex()
class A2SPAClient:
def __init__(self, api_base, api_key, agent_id, private_key_path, timeout=20):
self.api_base = api_base.rstrip("/")
self.api_key = api_key
self.agent_id = agent_id
self.private_key_path = private_key_path
self.timeout = timeout
self._private_key = None
@classmethod
def from_env(cls):
return cls(
api_base=os.environ.get("A2SPA_API_BASE", "https://aimodularity.com/A2SPA"),
api_key=os.environ["A2SPA_API_KEY"],
agent_id=os.environ["A2SPA_AGENT_ID"],
private_key_path=os.environ["A2SPA_PRIVATE_KEY_PATH"],
)
def _load_private_key(self):
if self._private_key is None:
with open(self.private_key_path, "rb") as handle:
self._private_key = serialization.load_pem_private_key(handle.read(), password=None)
return self._private_key
def build_payload_fields(self, target_agent_id, input_data, output_data, alert_threshold=10, extra=None):
payload = {
"agent_id": self.agent_id,
"target_agent_id": target_agent_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"nonce": str(uuid.uuid4()),
"input": input_data,
"output": output_data,
"alert_threshold": alert_threshold,
}
if extra:
payload.update(extra)
return payload
def build_payload(self, target_agent_id, input_data, output_data, alert_threshold=10, extra=None):
return finalize_payload(
self.build_payload_fields(target_agent_id, input_data, output_data, alert_threshold, extra)
)
def build_signed_request(self, target_agent_id, input_data, output_data, alert_threshold=10, extra=None):
payload_before_hash = self.build_payload_fields(
target_agent_id,
input_data,
output_data,
alert_threshold,
extra,
)
payload = finalize_payload(payload_before_hash)
signature = sign_payload(self._load_private_key(), payload_before_hash)
return {
"payload_before_hash": payload_before_hash,
"payload": payload,
"signature": signature,
"request_body": build_request_body(payload, signature),
}
def post_request_body(self, request_body):
return requests.post(
f"{self.api_base}/api/verify_payload",
headers={
"Content-Type": "application/json",
"x-api-key": self.api_key,
},
json=request_body,
timeout=self.timeout,
)
def send_payload(self, target_agent_id, input_data, output_data, alert_threshold=10, extra=None, debug=False):
signed_request = self.build_signed_request(
target_agent_id,
input_data,
output_data,
alert_threshold,
extra,
)
response = self.post_request_body(signed_request["request_body"])
if debug and response.status_code >= 400:
print(
json.dumps(
build_debug_snapshot(
signed_request["payload_before_hash"],
signed_request["signature"],
signed_request["payload"],
response.text,
),
indent=2,
sort_keys=True,
)
)
response.raise_for_status()
return response.json()
def fetch_logs(self, agent_id=None):
response = requests.get(
f"{self.api_base}/api/logs_for_agent",
headers={"x-api-key": self.api_key},
params={"agent_id": agent_id or self.agent_id},
timeout=self.timeout,
)
response.raise_for_status()
return response.json().get("logs", [])
def fetch_inbox(self, agent_id=None, after=None, limit=50):
params = {"agent_id": agent_id or self.agent_id, "limit": limit}
if after:
params["after"] = after
response = requests.get(
f"{self.api_base}/api/inbox_for_agent",
headers={"x-api-key": self.api_key},
params=params,
timeout=self.timeout,
)
response.raise_for_status()
return response.json().get("messages", [])
def run_and_send(self, target_agent_id, input_data, runner, alert_threshold=10):
output_data = runner(input_data)
return self.send_payload(target_agent_id, input_data, output_data, alert_threshold)
def existing_agent(input_data):
return {
"status": "ready",
"summary": f"Processed: {input_data.get('message', 'no message')}",
}
if __name__ == "__main__":
client = A2SPAClient.from_env()
target_agent_id = os.environ["A2SPA_TARGET_AGENT_ID"]
input_data = {"message": "Hello from my existing Python agent"}
output_data = existing_agent(input_data)
signed_request = client.build_signed_request(
target_agent_id,
input_data,
output_data,
)
first_response = client.post_request_body(signed_request["request_body"])
print(f"First request: {first_response.status_code} {first_response.text}")
second_response = client.post_request_body(signed_request["request_body"])
print(f"Second request: {second_response.status_code} {second_response.text}")
import fs from "node:fs/promises";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";
const EXPECTED_HASH_RE = /expected:\s*([0-9a-f]{64})/i;
export const SIGNABLE_FIELDS = Object.freeze([
"agent_id",
"target_agent_id",
"timestamp",
"nonce",
"input",
"output",
"alert_threshold",
]);
function normalizeA2spaPayload(payload) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
throw new TypeError("payload must be an object");
}
const normalized = {};
for (const field of SIGNABLE_FIELDS) {
if (Object.prototype.hasOwnProperty.call(payload, field)) {
const value = payload[field];
if (value === undefined) {
throw new TypeError(`A2SPA signable field cannot be undefined: ${field}`);
}
normalized[field] = value;
}
}
if (!Object.prototype.hasOwnProperty.call(normalized, "alert_threshold")) {
normalized.alert_threshold = 10;
}
return normalized;
}
function pythonCompatibleJsonString(value) {
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (character) => {
return "\\u" + character.charCodeAt(0).toString(16).padStart(4, "0");
});
}
export function canonicalJsonPythonStyle(value) {
if (value === null) return "null";
if (Array.isArray(value)) {
return `[${value.map((item) => canonicalJsonPythonStyle(item)).join(", ")}]`;
}
if (typeof value === "object") {
const keys = Object.keys(value).sort();
return `{${keys
.map((key) => `${pythonCompatibleJsonString(key)}: ${canonicalJsonPythonStyle(value[key])}`)
.join(", ")}}`;
}
if (typeof value === "string") {
return pythonCompatibleJsonString(value);
}
if (typeof value === "number") {
if (!Number.isFinite(value)) {
throw new TypeError("A2SPA canonicalization only supports finite JSON numbers");
}
return String(value);
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
throw new TypeError(`Unsupported payload value type: ${typeof value}`);
}
export function canonicalizeA2spaValue(value) {
return canonicalJsonPythonStyle(value);
}
export function canonicalizeA2spaPayload(payload) {
return canonicalJsonPythonStyle(normalizeA2spaPayload(payload));
}
export function prepareA2spaSignedBytes(payload) {
return Buffer.from(canonicalizeA2spaPayload(payload), "utf8");
}
export const prepareSignedBytes = prepareA2spaSignedBytes;
export function computeA2spaPayloadHash(payload) {
return crypto.createHash("sha256").update(prepareA2spaSignedBytes(payload)).digest("hex");
}
export const computePayloadHash = computeA2spaPayloadHash;
export function finalizeA2spaPayload(payloadBeforeHash) {
const finalized = { ...payloadBeforeHash };
delete finalized.hash;
delete finalized.signature;
finalized.hash = computeA2spaPayloadHash(payloadBeforeHash);
return finalized;
}
export const finalizePayload = finalizeA2spaPayload;
export function buildA2spaRequestBody(payload, signature) {
return { payload, signature };
}
export const buildRequestBody = buildA2spaRequestBody;
export function extractExpectedHash(errorText) {
if (!errorText) return null;
const match = String(errorText).match(EXPECTED_HASH_RE);
return match ? match[1] : null;
}
export function buildDebugSnapshot(payloadBeforeHash, signature = null, finalPayload = null, serverErrorText = null) {
const canonicalString = canonicalizeA2spaPayload(payloadBeforeHash);
const signedBytes = prepareA2spaSignedBytes(payloadBeforeHash);
const requestPayload = finalPayload ?? finalizeA2spaPayload(payloadBeforeHash);
const snapshot = {
payload_before_hash: payloadBeforeHash,
canonical_string: canonicalString,
canonical_bytes_hex: signedBytes.toString("hex"),
client_computed_hash: computeA2spaPayloadHash(payloadBeforeHash),
final_request_body: signature ? buildA2spaRequestBody(requestPayload, signature) : { payload: requestPayload },
server_expected_hash: extractExpectedHash(serverErrorText),
};
if (serverErrorText !== null && serverErrorText !== undefined) {
snapshot.server_error = serverErrorText;
}
return snapshot;
}
export function signA2spaPayload(payload, privateKeyPem) {
const signer = crypto.createSign("RSA-SHA256");
signer.update(prepareA2spaSignedBytes(payload));
signer.end();
return signer.sign(privateKeyPem, "hex");
}
export const signPayload = signA2spaPayload;
export class A2SPAClient {
constructor({ apiBase, apiKey, agentId, privateKeyPath, timeoutMs = 20000 }) {
this.apiBase = apiBase.replace(/\/$/, "");
this.apiKey = apiKey;
this.agentId = agentId;
this.privateKeyPath = privateKeyPath;
this.timeoutMs = timeoutMs;
this.privateKeyPem = null;
}
static fromEnv() {
return new A2SPAClient({
apiBase: process.env.A2SPA_API_BASE || "https://aimodularity.com/A2SPA",
apiKey: process.env.A2SPA_API_KEY,
agentId: process.env.A2SPA_AGENT_ID,
privateKeyPath: process.env.A2SPA_PRIVATE_KEY_PATH,
});
}
async loadPrivateKeyPem() {
if (!this.privateKeyPem) {
this.privateKeyPem = await fs.readFile(this.privateKeyPath, "utf8");
}
return this.privateKeyPem;
}
buildPayloadFields(targetAgentId, input, output, alertThreshold = 10, extra = {}) {
const payload = {
agent_id: this.agentId,
target_agent_id: targetAgentId,
timestamp: new Date().toISOString(),
nonce: crypto.randomUUID(),
input,
output,
alert_threshold: alertThreshold,
...extra,
};
return payload;
}
buildPayload(targetAgentId, input, output, alertThreshold = 10, extra = {}) {
return finalizeA2spaPayload(
this.buildPayloadFields(targetAgentId, input, output, alertThreshold, extra),
);
}
async buildSignedRequest(targetAgentId, input, output, alertThreshold = 10, extra = {}) {
const payloadBeforeHash = this.buildPayloadFields(targetAgentId, input, output, alertThreshold, extra);
const payload = finalizeA2spaPayload(payloadBeforeHash);
const signature = signA2spaPayload(payloadBeforeHash, await this.loadPrivateKeyPem());
return {
payloadBeforeHash,
payload,
signature,
requestBody: buildA2spaRequestBody(payload, signature),
};
}
async postRequestBody(requestBody) {
return fetch(`${this.apiBase}/api/verify_payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": this.apiKey,
},
body: JSON.stringify(requestBody),
});
}
async sendPayload(targetAgentId, input, output, alertThreshold = 10, extra = {}, debug = false) {
const signedRequest = await this.buildSignedRequest(targetAgentId, input, output, alertThreshold, extra);
const response = await this.postRequestBody(signedRequest.requestBody);
if (!response.ok) {
const errorText = await response.text();
if (debug) {
console.error(
JSON.stringify(
buildDebugSnapshot(
signedRequest.payloadBeforeHash,
signedRequest.signature,
signedRequest.payload,
errorText,
),
null,
2,
),
);
}
throw new Error(errorText);
}
return response.json();
}
async fetchLogs(agentId = this.agentId) {
const params = new URLSearchParams({ agent_id: agentId });
const response = await fetch(`${this.apiBase}/api/logs_for_agent?${params.toString()}`, {
headers: { "x-api-key": this.apiKey },
});
if (!response.ok) {
throw new Error(await response.text());
}
const body = await response.json();
return body.logs || [];
}
async fetchInbox(agentId = this.agentId, after = "", limit = 50) {
const params = new URLSearchParams({ agent_id: agentId, limit: String(limit) });
if (after) params.set("after", after);
const response = await fetch(`${this.apiBase}/api/inbox_for_agent?${params.toString()}`, {
headers: { "x-api-key": this.apiKey },
});
if (!response.ok) {
throw new Error(await response.text());
}
const body = await response.json();
return body.messages || [];
}
async runAndSend(targetAgentId, input, runner, alertThreshold = 10) {
const output = await runner(input);
return this.sendPayload(targetAgentId, input, output, alertThreshold);
}
}
async function existingAgent(input) {
return {
status: "ready",
summary: `Processed: ${input.message ?? "no message"}`,
};
}
async function main() {
const client = A2SPAClient.fromEnv();
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()}`);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
import fs from "node:fs/promises";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";
const EXPECTED_HASH_RE = /expected:\s*([0-9a-f]{64})/i;
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
interface SendResult {
success?: boolean;
error?: string;
}
interface LogEntry {
action?: string;
[key: string]: unknown;
}
export const SIGNABLE_FIELDS = [
"agent_id",
"target_agent_id",
"timestamp",
"nonce",
"input",
"output",
"alert_threshold",
] as const;
export function normalizeA2spaPayload(payload: { [key: string]: JsonValue | undefined }): { [key: string]: JsonValue } {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
throw new TypeError("payload must be an object");
}
const normalized: { [key: string]: JsonValue } = {};
for (const field of SIGNABLE_FIELDS) {
if (Object.prototype.hasOwnProperty.call(payload, field)) {
const value = payload[field];
if (value === undefined) {
throw new TypeError(`A2SPA signable field cannot be undefined: ${field}`);
}
normalized[field] = value;
}
}
if (!Object.prototype.hasOwnProperty.call(normalized, "alert_threshold")) {
normalized.alert_threshold = 10;
}
return normalized;
}
function pythonCompatibleJsonString(value: string): string {
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (character) => {
return "\\u" + character.charCodeAt(0).toString(16).padStart(4, "0");
});
}
export function canonicalJsonPythonStyle(value: JsonValue | undefined): string {
if (value === null) return "null";
if (Array.isArray(value)) {
return `[${value.map((item) => canonicalJsonPythonStyle(item)).join(", ")}]`;
}
if (typeof value === "object") {
if (!value) return "null";
const record = value as { [key: string]: JsonValue };
const keys = Object.keys(record).sort();
return `{${keys
.map((key) => `${pythonCompatibleJsonString(key)}: ${canonicalJsonPythonStyle(record[key])}`)
.join(", ")}}`;
}
if (typeof value === "string") {
return pythonCompatibleJsonString(value);
}
if (typeof value === "number") {
if (!Number.isFinite(value)) {
throw new TypeError("A2SPA canonicalization only supports finite JSON numbers");
}
return String(value);
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
throw new TypeError(`Unsupported payload value type: ${typeof value}`);
}
export function canonicalizeA2spaValue(value: JsonValue | undefined): string {
return canonicalJsonPythonStyle(value);
}
export function canonicalizeA2spaPayload(payload: { [key: string]: JsonValue | undefined }): string {
return canonicalJsonPythonStyle(normalizeA2spaPayload(payload));
}
export function prepareA2spaSignedBytes(payload: { [key: string]: JsonValue | undefined }): Buffer {
return Buffer.from(canonicalizeA2spaPayload(payload), "utf8");
}
export const prepareSignedBytes = prepareA2spaSignedBytes;
export function computeA2spaPayloadHash(payload: { [key: string]: JsonValue | undefined }): string {
return crypto.createHash("sha256").update(prepareA2spaSignedBytes(payload)).digest("hex");
}
export const computePayloadHash = computeA2spaPayloadHash;
export function finalizeA2spaPayload(payloadBeforeHash: { [key: string]: JsonValue | undefined }): { [key: string]: JsonValue } {
const finalized: { [key: string]: JsonValue | undefined } = { ...payloadBeforeHash };
delete finalized.hash;
delete finalized.signature;
finalized.hash = computeA2spaPayloadHash(payloadBeforeHash);
return finalized as { [key: string]: JsonValue };
}
export const finalizePayload = finalizeA2spaPayload;
export function buildA2spaRequestBody(payload: { [key: string]: JsonValue }, signature: string): {
payload: { [key: string]: JsonValue };
signature: string;
} {
return { payload, signature };
}
export const buildRequestBody = buildA2spaRequestBody;
export function extractExpectedHash(errorText: string | null | undefined): string | null {
if (!errorText) return null;
const match = String(errorText).match(EXPECTED_HASH_RE);
return match ? match[1] : null;
}
export function buildDebugSnapshot(
payloadBeforeHash: { [key: string]: JsonValue | undefined },
signature: string | null = null,
finalPayload: { [key: string]: JsonValue } | null = null,
serverErrorText: string | null = null,
): { [key: string]: unknown } {
const canonicalString = canonicalizeA2spaPayload(payloadBeforeHash);
const signedBytes = prepareA2spaSignedBytes(payloadBeforeHash);
const requestPayload = finalPayload ?? finalizeA2spaPayload(payloadBeforeHash);
const snapshot: { [key: string]: unknown } = {
payload_before_hash: payloadBeforeHash,
canonical_string: canonicalString,
canonical_bytes_hex: signedBytes.toString("hex"),
client_computed_hash: computeA2spaPayloadHash(payloadBeforeHash),
final_request_body: signature ? buildA2spaRequestBody(requestPayload, signature) : { payload: requestPayload },
server_expected_hash: extractExpectedHash(serverErrorText),
};
if (serverErrorText !== null) {
snapshot.server_error = serverErrorText;
}
return snapshot;
}
export function signA2spaPayload(payload: { [key: string]: JsonValue | undefined }, privateKeyPem: string): string {
const signer = crypto.createSign("RSA-SHA256");
signer.update(prepareA2spaSignedBytes(payload));
signer.end();
return signer.sign(privateKeyPem, "hex");
}
export const signPayload = signA2spaPayload;
export class A2SPAClient {
private apiBase: string;
private apiKey: string;
private agentId: string;
private privateKeyPath: string;
private privateKeyPem?: string;
constructor(options: { apiBase: string; apiKey: string; agentId: string; privateKeyPath: string }) {
this.apiBase = options.apiBase.replace(/\/$/, "");
this.apiKey = options.apiKey;
this.agentId = options.agentId;
this.privateKeyPath = options.privateKeyPath;
}
static fromEnv(): A2SPAClient {
return new A2SPAClient({
apiBase: process.env.A2SPA_API_BASE || "https://aimodularity.com/A2SPA",
apiKey: process.env.A2SPA_API_KEY || "",
agentId: process.env.A2SPA_AGENT_ID || "",
privateKeyPath: process.env.A2SPA_PRIVATE_KEY_PATH || "",
});
}
private async loadPrivateKeyPem(): Promise<string> {
if (!this.privateKeyPem) {
this.privateKeyPem = await fs.readFile(this.privateKeyPath, "utf8");
}
return this.privateKeyPem;
}
buildPayloadFields(
targetAgentId: string,
input: JsonValue,
output: JsonValue,
alertThreshold = 10,
extra: { [key: string]: JsonValue } = {},
): { [key: string]: JsonValue } {
const payload: { [key: string]: JsonValue } = {
agent_id: this.agentId,
target_agent_id: targetAgentId,
timestamp: new Date().toISOString(),
nonce: crypto.randomUUID(),
input,
output,
alert_threshold: alertThreshold,
...extra,
};
return payload;
}
buildPayload(
targetAgentId: string,
input: JsonValue,
output: JsonValue,
alertThreshold = 10,
extra: { [key: string]: JsonValue } = {},
): { [key: string]: JsonValue } {
return finalizeA2spaPayload(
this.buildPayloadFields(targetAgentId, input, output, alertThreshold, extra),
);
}
async buildSignedRequest(
targetAgentId: string,
input: JsonValue,
output: JsonValue,
alertThreshold = 10,
extra: { [key: string]: JsonValue } = {},
): Promise<{
payloadBeforeHash: { [key: string]: JsonValue };
payload: { [key: string]: JsonValue };
signature: string;
requestBody: { payload: { [key: string]: JsonValue }; signature: string };
}> {
const payloadBeforeHash = this.buildPayloadFields(targetAgentId, input, output, alertThreshold, extra);
const payload = finalizeA2spaPayload(payloadBeforeHash);
const signature = signA2spaPayload(payloadBeforeHash, await this.loadPrivateKeyPem());
return {
payloadBeforeHash,
payload,
signature,
requestBody: buildA2spaRequestBody(payload, signature),
};
}
async postRequestBody(
requestBody: { payload: { [key: string]: JsonValue }; signature: string },
): Promise<Response> {
return fetch(`${this.apiBase}/api/verify_payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": this.apiKey,
},
body: JSON.stringify(requestBody),
});
}
async sendPayload(
targetAgentId: string,
input: JsonValue,
output: JsonValue,
alertThreshold = 10,
extra: { [key: string]: JsonValue } = {},
debug = false,
): Promise<SendResult> {
const signedRequest = await this.buildSignedRequest(targetAgentId, input, output, alertThreshold, extra);
const response = await this.postRequestBody(signedRequest.requestBody);
if (!response.ok) {
const errorText = await response.text();
if (debug) {
console.error(
JSON.stringify(
buildDebugSnapshot(
signedRequest.payloadBeforeHash,
signedRequest.signature,
signedRequest.payload,
errorText,
),
null,
2,
),
);
}
throw new Error(errorText);
}
return (await response.json()) as SendResult;
}
async fetchLogs(agentId = this.agentId): Promise<LogEntry[]> {
const params = new URLSearchParams({ agent_id: agentId });
const response = await fetch(`${this.apiBase}/api/logs_for_agent?${params.toString()}`, {
headers: { "x-api-key": this.apiKey },
});
if (!response.ok) {
throw new Error(await response.text());
}
const body = (await response.json()) as { logs?: LogEntry[] };
return body.logs || [];
}
async fetchInbox(agentId = this.agentId, after = "", limit = 50): Promise<LogEntry[]> {
const params = new URLSearchParams({ agent_id: agentId, limit: String(limit) });
if (after) params.set("after", after);
const response = await fetch(`${this.apiBase}/api/inbox_for_agent?${params.toString()}`, {
headers: { "x-api-key": this.apiKey },
});
if (!response.ok) {
throw new Error(await response.text());
}
const body = (await response.json()) as { messages?: LogEntry[] };
return body.messages || [];
}
async runAndSend(
targetAgentId: string,
input: JsonValue,
runner: (input: JsonValue) => Promise<JsonValue> | JsonValue,
alertThreshold = 10,
): Promise<SendResult> {
const output = await runner(input);
return this.sendPayload(targetAgentId, input, output, alertThreshold);
}
}
async function existingAgent(input: JsonValue): Promise<JsonValue> {
const message = typeof input === "object" && input && !Array.isArray(input) ? (input as { message?: string }).message : undefined;
return {
status: "ready",
summary: `Processed: ${message ?? "no message"}`,
};
}
async function main(): Promise<void> {
const client = A2SPAClient.fromEnv();
const input = { message: "Hello from my existing TypeScript 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()}`);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
{
"ai_workflow": [
"If a browser session is available, open the Builder Pack to create or rotate the needed agents first.",
"Save the returned private key immediately; it is only shown once.",
"Add the A2SPA environment variables to the target project.",
"Install one of the starter templates and wrap the existing agent with the provided send and fetch helpers.",
"Use the inbox_for_agent endpoint to poll received messages for the receiving agent."
],
"api_base": "https://aimodularity.com/A2SPA",
"code_generation_rules": [
"Always send signed payloads through POST /api/verify_payload.",
"Always fetch received runtime messages through GET /api/inbox_for_agent.",
"Use GET /api/logs_for_agent for audit and troubleshooting, not as the main runtime inbox.",
"Do not verify payloads locally as a substitute for the API.",
"Do not read or write Firestore directly from customer agent code.",
"Do not bypass nonce, timestamp, signature, permission, or toggle checks.",
"Store the agent private key outside source control and outside public directories."
],
"discovery_urls": {
"docs_url": "https://aimodularity.com/A2SPA/docs",
"integration_page_url": "https://aimodularity.com/A2SPA/integrations",
"integration_spec_url": "https://aimodularity.com/A2SPA/integrations/spec.json",
"well_known_url": "https://aimodularity.com/A2SPA/.well-known/a2spa-integration.json"
},
"environment_template_url": "https://aimodularity.com/A2SPA/integrations/templates/env",
"must_use_a2spa_api": true,
"platform": "A2SPA",
"provisioning": {
"builder_pack_url": "https://aimodularity.com/A2SPA/agent-builder",
"builder_spec_url": "https://aimodularity.com/A2SPA/api/agent_builder_spec",
"requires_authenticated_browser_session": true,
"summary": "Use the Builder Pack when an AI assistant has access to the logged-in dashboard session and needs to create, rotate, revoke, or toggle agents."
},
"purpose": "Teach AI coding agents how to provision A2SPA agents through the dashboard Builder Pack and how to integrate existing agents using the public A2SPA API only.",
"runtime_api": {
"inbox_for_agent": {
"headers": {
"x-api-key": "\u003cdashboard API key\u003e"
},
"method": "GET",
"notes": [
"Use this to poll received messages for an agent through the A2SPA API.",
"This is the preferred runtime retrieval path for receiving agents."
],
"query": {
"after": "optional ISO timestamp for polling",
"agent_id": "\u003cagent id to inspect\u003e",
"limit": 50
},
"url": "https://aimodularity.com/A2SPA/api/inbox_for_agent"
},
"logs_for_agent": {
"headers": {
"x-api-key": "\u003cdashboard API key\u003e"
},
"method": "GET",
"notes": [
"Use this for audit logs, troubleshooting, and dashboard-style history.",
"Do not use this as the primary runtime inbox when inbox_for_agent is available."
],
"query": {
"agent_id": "\u003cagent id to inspect\u003e"
},
"url": "https://aimodularity.com/A2SPA/api/logs_for_agent"
},
"verify_payload": {
"canonicalization": {
"array_order_preserved": true,
"compute_hash_before_setting_hash_field": true,
"excluded_fields": [
"hash",
"signature",
"extra metadata not in the current verifier schema"
],
"hash_algorithm": "sha256",
"included_fields": [
"agent_id",
"target_agent_id",
"timestamp",
"nonce",
"input",
"output",
"alert_threshold"
],
"number_handling": "Use finite JSON numbers; send exact decimal spellings as strings when cross-language lexical stability matters",
"recursive_key_sorting": true,
"signature_algorithm": "RSA PKCS1v15 SHA256",
"string_escaping": "Python json.dumps(..., sort_keys=True) with ensure_ascii=True semantics",
"timestamp_format": "ISO-8601 UTC string",
"utf8_bytes": true,
"whitespace": "Python default separators: comma+space and colon+space"
},
"headers": {
"Content-Type": "application/json",
"x-api-key": "\u003cdashboard API key\u003e"
},
"method": "POST",
"request_json": {
"payload": {
"agent_id": "\u003csender agent id\u003e",
"alert_threshold": 10,
"hash": "sha256 over the UTF-8 bytes of Python-compatible canonical JSON for the signable payload",
"input": {
"message": "hello"
},
"nonce": "uuid",
"output": {
"status": "ready"
},
"target_agent_id": "\u003creceiver agent id\u003e",
"timestamp": "ISO-8601 string"
},
"signature": "hex RSA PKCS1v15 SHA256 signature over the same canonical UTF-8 bytes"
},
"url": "https://aimodularity.com/A2SPA/api/verify_payload"
}
},
"security_model": {
"dashboard_management_source": "A2SPA dashboard Builder Pack only",
"delegation_boundary": [
"on_behalf_of is never authorization by itself. It is metadata unless backed by a verified delegation proof.",
"Cryptographic binding proves integrity. Policy determines semantic permission.",
"A future delegated execution design must require a second signature, capability token, or explicit delegation policy object."
],
"forbidden_shortcuts": [
"Direct Firestore writes",
"Skipping the verify_payload API",
"Embedding private keys in git",
"Replacing server-side verification with local-only checks"
],
"runtime_verification_source": "A2SPA API only"
},
"spec_kind": "api_integration_pack",
"spec_version": "2026-03-23",
"starter_kits": {
"env": {
"filename": ".env.a2spa.example",
"label": "Environment",
"runtime": "Generic",
"url": "https://aimodularity.com/A2SPA/integrations/templates/env"
},
"javascript": {
"filename": "a2spa-client.mjs",
"label": "JavaScript Starter",
"runtime": "Node.js 18+",
"url": "https://aimodularity.com/A2SPA/integrations/templates/javascript"
},
"python": {
"filename": "a2spa_client.py",
"label": "Python Starter",
"runtime": "Python",
"url": "https://aimodularity.com/A2SPA/integrations/templates/python"
},
"typescript": {
"filename": "a2spa-client.ts",
"label": "TypeScript Starter",
"runtime": "Node.js 18+",
"url": "https://aimodularity.com/A2SPA/integrations/templates/typescript"
}
}
}