A2SPA API Integration Pack

Teach AI coding agents to wire existing agents into A2SPA.

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.

Runtime integrations must use the A2SPA API. Do not generate code that writes directly to Firestore, skips POST /api/verify_payload, or replaces server-side verification with local-only checks.

1. Provision Agents

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.

2. Wrap Existing Code

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.

3. Stay Inside the API

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.

AI Workflow

Step 1

If a browser session is available, open the Builder Pack to create or rotate the needed agents first.

Step 2

Save the returned private key immediately; it is only shown once.

Step 3

Add the A2SPA environment variables to the target project.

Step 4

Install one of the starter templates and wrap the existing agent with the provided send and fetch helpers.

Step 5

Use the inbox_for_agent endpoint to poll received messages for the receiving agent.

API-Only Rules

Required

Always send signed payloads through POST /api/verify_payload.

Required

Always fetch received runtime messages through GET /api/inbox_for_agent.

Required

Use GET /api/logs_for_agent for audit and troubleshooting, not as the main runtime inbox.

Required

Do not verify payloads locally as a substitute for the API.

Required

Do not read or write Firestore directly from customer agent code.

Required

Do not bypass nonce, timestamp, signature, permission, or toggle checks.

Required

Store the agent private key outside source control and outside public directories.

Supported Runtime Endpoints

Send + Verify 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.

Poll Agent Inbox 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.

Audit + Troubleshoot 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.

Starter Kits

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.

Generic

Environment

.env.a2spa.example
Python

Python Starter

a2spa_client.py
Node.js 18+

JavaScript Starter

a2spa-client.mjs
Node.js 18+

TypeScript Starter

a2spa-client.ts

Environment Template

.env.a2spa.example
Open Raw
A2SPA_API_BASE=https://aimodularity.com/A2SPA
A2SPA_API_KEY=replace-with-dashboard-api-key
A2SPA_AGENT_ID=replace-with-your-agent-id
A2SPA_PRIVATE_KEY_PATH=/absolute/path/to/your-agent.priv.pem
A2SPA_TARGET_AGENT_ID=replace-when-you-send-to-another-agent

Python Starter Preview

a2spa_client.py
Open Raw
import hashlib
import json
import os
import uuid
from datetime import datetime, timezone

import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


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 _canonical_json(self, value):
        return json.dumps(value, sort_keys=True)

    def _sha256_hex(self, value):
        clean = {k: v for k, v in value.items() if k not in ("hash", "signature")}
        return hashlib.sha256(self._canonical_json(clean).encode()).hexdigest()

    def _sign(self, payload):
        clean = {k: v for k, v in payload.items() if k not in ("hash", "signature")}
        message = self._canonical_json(clean).encode()
        return self._load_private_key().sign(
            message,
            padding.PKCS1v15(),
            hashes.SHA256(),
        ).hex()

    def build_payload(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)
        payload["hash"] = self._sha256_hex(payload)
        return payload

    def send_payload(self, target_agent_id, input_data, output_data, alert_threshold=10, extra=None):
        payload = self.build_payload(target_agent_id, input_data, output_data, alert_threshold, extra)
        signature = self._sign(payload)
        response = requests.post(
            f"{self.api_base}/api/verify_payload",
            headers={
                "Content-Type": "application/json",
                "x-api-key": self.api_key,
            },
            json={"payload": payload, "signature": signature},
            timeout=self.timeout,
        )
        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"]
    result = client.run_and_send(
        target_agent_id,
        {"message": "Hello from my existing Python agent"},
        existing_agent,
    )
    print(result)

JavaScript Starter Preview

a2spa-client.mjs
Open Raw
import fs from "node:fs/promises";
import crypto from "node:crypto";


function canonicalJson(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) {
    return `[${value.map((item) => canonicalJson(item)).join(", ")}]`;
  }
  if (typeof value === "object") {
    const keys = Object.keys(value).sort();
    return `{${keys
      .map((key) => `${JSON.stringify(key)}: ${canonicalJson(value[key])}`)
      .join(", ")}}`;
  }
  return JSON.stringify(value);
}


function stripSecurityFields(payload) {
  const { hash, signature, ...rest } = payload;
  return rest;
}


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;
  }

  sha256Hex(value) {
    return crypto.createHash("sha256").update(canonicalJson(stripSecurityFields(value))).digest("hex");
  }

  async sign(payload) {
    const signer = crypto.createSign("RSA-SHA256");
    signer.update(canonicalJson(stripSecurityFields(payload)));
    signer.end();
    return signer.sign(await this.loadPrivateKeyPem(), "hex");
  }

  buildPayload(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,
    };
    payload.hash = this.sha256Hex(payload);
    return payload;
  }

  async sendPayload(targetAgentId, input, output, alertThreshold = 10, extra = {}) {
    const payload = this.buildPayload(targetAgentId, input, output, alertThreshold, extra);
    const signature = await this.sign(payload);
    const response = await fetch(`${this.apiBase}/api/verify_payload`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": this.apiKey,
      },
      body: JSON.stringify({ payload, signature }),
    });
    if (!response.ok) {
      throw new Error(await response.text());
    }
    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"}`,
  };
}


if (import.meta.url === `file://${process.argv[1]}`) {
  const client = A2SPAClient.fromEnv();
  const result = await client.runAndSend(
    process.env.A2SPA_TARGET_AGENT_ID,
    { message: "Hello from my existing Node agent" },
    existingAgent,
  );
  console.log(result);
}

TypeScript Starter Preview

a2spa-client.ts
Open Raw
import fs from "node:fs/promises";
import crypto from "node:crypto";


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;
}

function canonicalJson(value: JsonValue): string {
  if (value === null) return "null";
  if (Array.isArray(value)) {
    return `[${value.map((item) => canonicalJson(item)).join(", ")}]`;
  }
  if (typeof value === "object") {
    const record = value as { [key: string]: JsonValue };
    const keys = Object.keys(record).sort();
    return `{${keys
      .map((key) => `${JSON.stringify(key)}: ${canonicalJson(record[key])}`)
      .join(", ")}}`;
  }
  return JSON.stringify(value);
}


function stripSecurityFields(payload: { [key: string]: JsonValue }): { [key: string]: JsonValue } {
  const { hash, signature, ...rest } = payload;
  return rest;
}


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;
  }

  private sha256Hex(value: { [key: string]: JsonValue }): string {
    return crypto.createHash("sha256").update(canonicalJson(stripSecurityFields(value))).digest("hex");
  }

  private async sign(payload: { [key: string]: JsonValue }): Promise<string> {
    const signer = crypto.createSign("RSA-SHA256");
    signer.update(canonicalJson(stripSecurityFields(payload)));
    signer.end();
    return signer.sign(await this.loadPrivateKeyPem(), "hex");
  }

  buildPayload(targetAgentId: string, input: JsonValue, output: JsonValue, alertThreshold = 10): { [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,
    };
    payload.hash = this.sha256Hex(payload);
    return payload;
  }

  async sendPayload(targetAgentId: string, input: JsonValue, output: JsonValue, alertThreshold = 10): Promise<SendResult> {
    const payload = this.buildPayload(targetAgentId, input, output, alertThreshold);
    const signature = await this.sign(payload);
    const response = await fetch(`${this.apiBase}/api/verify_payload`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": this.apiKey,
      },
      body: JSON.stringify({ payload, signature }),
    });
    if (!response.ok) {
      throw new Error(await response.text());
    }
    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 result = await client.runAndSend(
    process.env.A2SPA_TARGET_AGENT_ID || "",
    { message: "Hello from my existing TypeScript agent" },
    existingAgent,
  );
  console.log(result);
}


void main();

Machine-Readable Spec

a2spa-integration.json
{
  "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": {
      "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 canonical payload without hash/signature",
          "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 canonical payload without hash/signature"
      },
      "url": "https://aimodularity.com/A2SPA/api/verify_payload"
    }
  },
  "security_model": {
    "dashboard_management_source": "A2SPA dashboard Builder Pack only",
    "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"
    }
  }
}