# AI100WEB A2A + x402 developer docs (staging)

> Simulation — no real funds. On staging x402 payments are simulated: nothing is built, signed or broadcast.
> Examples verified 04 Nov 2026, 22:30 ICT against https://a2a-docs-preview.ai100web.pages.dev (14/14 passed). HTML version: https://ai100web.pages.dev/agents/docs

## 1. Discover
- Agent Card: `GET https://ai100web.pages.dev/.well-known/agent-card.json` (also `/agent-card.json`, `/.well-known/agent.json`)
- Gateway status: `GET https://ai100web.pages.dev/a2a` with `accept: application/json` (x402.simulation = true)
- A2A endpoint: `https://ai100web.pages.dev/a2a` — JSON-RPC 2.0 `message/send`; select a skill with a data part `{"skill":"<id>", ...}`

## 2. Skills and prices
| skill | price | description |
|---|---|---|
| `check-balance` | free | Get balance of the owner wallet, its Agentic Wallets, or any address |
| `resolve-domain` | free | Resolve .ton domain to wallet or records |
| `transfer-ton` | free | Prepare an UNSIGNED TON transfer (simulation / ready-to-sign preview). Never signs or broadcasts; execute always becomes pending_approval. |
| `transfer-simulate` | free | Dry-run a TON transfer: address checksum, amount and comment validation plus a fee estimate. simulated:true, never signed or broadcast. JSON-RPC: thin alias of transfer-ton with mode forced to "simulation". Free. |
| `ton-balance` | free | TON balance of an address. Thin alias of check-balance (same handler, same params: {address?}). Free, answered at the gateway (simulated data). |
| `ton-dns-resolve` | free | Resolve a .ton domain to its wallet. Thin alias of resolve-domain (same handler, same params: {domain}). Free, answered at the gateway (simulated data). |
| `ai-analyze` | 0.01 TON (10000000 nano, x402) | Analyse a wallet, token, or dataset and return a structured report |
| `ai-summarize` | 0.01 TON (10000000 nano, x402) | Condense text or documents into a faithful summary |
| `ai-content` | 0.01 TON (10000000 nano, x402) | Produce polished, ready-to-use content from a brief |
| `ai-orchestrate` | 0.01 TON (10000000 nano, x402) | Plan a goal into subtasks, run an agent team, and synthesize a complete result |
| `ai-orchestrate-plan` | free | Free PLAN step of ai-orchestrate: returns which sub-agents (ai-orchestrate.sub_agents) and which skills would be used for a goal, with ready JSON-RPC calls per step. Executes, forwards, signs and charges nothing. Params: {goal (1..500 chars), address?, to?, amount?}. |
| `mcp-tool-call` | free | Run an allow-listed read-only MCP tool (ton_get_balance, ton_dns_resolve) on the AI100WEB MCP server and get its result back as A2A task artifacts. Signed envelope required. Nothing is signed or sent. |

Paid skills run at the edge after the payment verifies (engine `ai100web-edge-v1`). No LLM is configured on staging: language tasks return `mode: "degraded"` and list what is missing in `needs`.
- `ai-analyze`: input `address` (TON) · `values` (number[]) · `text` → mode full / partial / degraded
- `ai-summarize`: input `text` · `maxSentences` (1–10) → mode degraded
- `ai-content`: input `text` or `brief` · `format` → mode degraded
- `ai-orchestrate`: input `text` or `goal` → mode full / partial / degraded

## 3. Signing (Ed25519)
Headers on every POST to `/a2a` and `/mcp`: `X-A2A-Key-Id` (registered key id), `X-A2A-Timestamp` (unix seconds, ±300s), `X-A2A-Signature` (base64 Ed25519 over the UTF-8 string `${ts}.${rawBody}` — the exact bytes sent).
An identical signed request is refused (`401 replayed`): use a unique `id`/`messageId` and sign every request again. Rate limit: 5 POST/min per IP and per key id → 429 + Retry-After.

## 4. Registering a key
Generate an Ed25519 key locally and send only the public key (raw 32 bytes, base64) and your key id to the operator. Staging has no self-service registration; ids are allow-listed in `https://ai100web.pages.dev/config/client-keys.json`.

```bash
# Generate an Ed25519 key LOCALLY (Node 18+). Only the public key leaves your machine.
node -e "
const {generateKeyPairSync}=require('node:crypto'),fs=require('node:fs');
const {publicKey,privateKey}=generateKeyPairSync('ed25519');
fs.writeFileSync('a2a-key.json',JSON.stringify({seed:privateKey.export({format:'der',type:'pkcs8'}).subarray(16).toString('base64')}),{mode:0o600});
console.log('public key (raw 32 bytes, base64):',publicKey.export({format:'der',type:'spki'}).subarray(12).toString('base64'));"
```

## 5. x402 flow
1. Signed call without payment → HTTP 402, task `input-required`, headers `PAYMENT-REQUIRED` = `X-PAYMENT-REQUIRED` = base64(JSON).
2. Pick an `accepts[]` entry; read `extra.nonce` and `extra.expiry` (valid 300s).
3. Pay `maxAmountRequired` to `payTo` with comment `x402:<nonce>` — on staging the proof is simulated (`payload.simulated: true`).
4. Retry (newly signed) with `X-PAYMENT` (v1) or `PAYMENT-SIGNATURE` (v2) → HTTP 200 `completed`, receipt in `PAYMENT-RESPONSE`, result in `result.artifacts[0]`.

```json
{
  "x402Version": 1,
  "error": "X-PAYMENT (or PAYMENT-SIGNATURE) header is required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "ton-mainnet",
      "payTo": "UQBzg5yX7h5m-DLINTeWzxc5lwmPjJrgMtWisw4rRUDYdSXZ",
      "resource": "https://a2a-docs-preview.ai100web.pages.dev/a2a#ai-summarize",
      "description": "One call to paid skill ai-summarize",
      "mimeType": "application/json",
      "maxTimeoutSeconds": 300,
      "asset": "TON",
      "maxAmountRequired": "10000000",
      "extra": {
        "nonce": "<uuid>",
        "comment": "x402:<uuid>",
        "expiry": "<ISO time, +300 s>",
        "simulationMode": true,
        "binding": {
          "payer": null,
          "requesterBound": true,
          "note": "no payer declared: real (non-simulated) proofs will be rejected; re-quote with message.metadata[\"x402.payer\"]"
        },
        "decimals": 9,
        "amount": "0.01"
      }
    },
    {
      "asset": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
      "network": "ton-mainnet",
      "payTo": "UQBzg5yX7h5m-DLINTeWzxc5lwmPjJrgMtWisw4rRUDYdSXZ",
      "maxAmountRequired": "10000",
      "…": "same nonce/expiry"
    }
  ]
}
```

- `X-PAYMENT`: base64 `{"x402Version":1,"scheme":"exact","network":"ton-mainnet","payload":{"simulated":true,"nonce":"<nonce>"}}`
- `PAYMENT-SIGNATURE`: base64 `{"x402Version":2,"accepted":<the TON accepts[] entry>,"payload":{"simulated":true,"nonce":"<nonce>"}}`
- Replay of a used proof → 402 `replayed_nonce` (skill not run). Expired → 402 `expired_nonce`. Unknown nonce → 402 `unknown_nonce`. Bad/conflicting header → 402 `invalid_payment_header`.

Receipt (decoded):
```json
{
  "receiptId": "<uuid>",
  "nonce": "<the quote nonce>",
  "skillId": "ai-summarize",
  "resource": "https://a2a-docs-preview.ai100web.pages.dev/a2a#ai-summarize",
  "payTo": "UQBzg5yX7h5m-DLINTeWzxc5lwmPjJrgMtWisw4rRUDYdSXZ",
  "payer": null,
  "asset": "TON",
  "amount": "10000000",
  "txHash": null,
  "network": "ton-mainnet",
  "simulated": true,
  "verifiedAt": "<ISO time>",
  "proof": {
    "alg": "SHA-256",
    "signed": false,
    "digest": "<sha-256 hex>",
    "note": "unsigned digest; set RECEIPT_SECRET for HMAC-SHA256"
  }
}
```
Result data part:
```json
{
  "executed": true,
  "paid": true,
  "skill": "ai-summarize",
  "engine": "ai100web-edge-v1",
  "mode": "degraded",
  "readOnly": true,
  "signed": false,
  "broadcast": false,
  "result": {
    "kind": "extractive",
    "summary": "Agents discover the card, sign each request with Ed25519, get an x402 quote, attach the proof and receive the result. On staging the payment is simulated. Nothing is signed on-chain.",
    "sentencesIn": 3,
    "sentencesOut": 3,
    "selected": [
      0,
      1,
      2
    ],
    "charactersIn": 182,
    "charactersOut": 182,
    "compression": 1,
    "keywords": [
      {
        "term": "agents",
        "count": 1
      },
      {
        "term": "attach",
        "count": 1
      },
      {
        "term": "card",
        "count": 1
      },
      {
        "term": "discover",
        "count": 1
      },
      {
        "term": "ed25519",
        "count": 1
      },
      {
        "term": "get",
        "count": 1
      },
      {
        "term": "nothing",
        "count": 1
      },
      {
        "term": "on-chain",
        "count": 1
      }
    ]
  },
  "limitations": [
    "Extractive: sentences are selected verbatim from the input by term-frequency scoring; nothing is paraphrased."
  ],
  "needs": [
    "abstractive / LLM-quality output needs an LLM provider API key (Pages secret) plus a provider integration; neither is configured"
  ],
  "receipt": "…same as PAYMENT-RESPONSE…",
  "quote": "…the quote terms…",
  "verification": {
    "checks": [
      {
        "check": "SIMULATED proof accepted (X402_SIMULATION=true); no on-chain verification performed",
        "ok": true
      }
    ],
    "source": null
  }
}
```

## 6. Examples
```js
# sign.mjs — print the X-A2A-Signature for "$TS.$BODY" (your seed stays on your machine)
#   node sign.mjs "$TS" "$BODY"
import { createPrivateKey, sign } from 'node:crypto'; import { readFileSync } from 'node:fs';
const seed = Buffer.from(JSON.parse(readFileSync(process.env.KEY_FILE || 'a2a-key.json', 'utf8')).seed, 'base64');
const key = createPrivateKey({ key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]), format: 'der', type: 'pkcs8' });
process.stdout.write(sign(null, Buffer.from(process.argv[2] + '.' + process.argv[3]), key).toString('base64'));
```
```bash
# Free skill. Placeholders only: <your-key-id> is the id the operator registered for your public key.
BODY='{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"role":"user","kind":"message","messageId":"m-'"$(date +%s)"'","parts":[{"kind":"data","data":{"skill":"resolve-domain","params":{"domain":"a2aprotocol.ton"}}}]}}}'
TS=$(date +%s)                                   # unix seconds; must be within ±300s of server time
SIG=$(node sign.mjs "$TS" "$BODY")               # base64 Ed25519 over "$TS.$BODY" (exact bytes sent)
curl -s https://ai100web.pages.dev/a2a -H "content-type: application/json" \
  -H "X-A2A-Key-Id: <your-key-id>" -H "X-A2A-Timestamp: $TS" -H "X-A2A-Signature: $SIG" \
  --data-binary "$BODY"
# → HTTP 200, result.status.state = "completed"
```
```bash
# Paid skill, step 1: same signed call, no payment → HTTP 402 + quote
BODY='{"jsonrpc":"2.0","id":"2","method":"message/send","params":{"message":{"role":"user","kind":"message","messageId":"m-'"$(date +%s)"'","parts":[{"kind":"data","data":{"skill":"ai-summarize","text":"Text to summarise ..."}}]}}}'
TS=$(date +%s); SIG=$(node sign.mjs "$TS" "$BODY")
curl -s -D headers.txt https://ai100web.pages.dev/a2a -H "content-type: application/json" \
  -H "X-A2A-Key-Id: <your-key-id>" -H "X-A2A-Timestamp: $TS" -H "X-A2A-Signature: $SIG" --data-binary "$BODY"
NONCE=$(grep -i '^payment-required:' headers.txt | cut -d' ' -f2 | tr -d '\r' | base64 -d \
  | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).accepts.find(a=>a.asset==="TON").extra.nonce))')

# Step 2: attach the proof. On staging the proof is SIMULATED (no transaction, no funds).
X_PAYMENT=$(printf '{"x402Version":1,"scheme":"exact","network":"ton-mainnet","payload":{"simulated":true,"nonce":"%s"}}' "$NONCE" | base64 | tr -d '\n')
TS=$(date +%s); SIG=$(node sign.mjs "$TS" "$BODY")   # fresh timestamp + signature (an identical signature is a replay)
curl -s -D receipt.txt https://ai100web.pages.dev/a2a -H "content-type: application/json" -H "X-PAYMENT: $X_PAYMENT" \
  -H "X-A2A-Key-Id: <your-key-id>" -H "X-A2A-Timestamp: $TS" -H "X-A2A-Signature: $SIG" --data-binary "$BODY"
# → HTTP 200, task "completed", header PAYMENT-RESPONSE = base64 receipt,
#   result.artifacts[0].parts[0].data = { executed: true, paid: true, mode, result, ... }
```
```js
// a2a-client.mjs — minimal AI100WEB A2A client (Node 18+, no dependencies).
//   KEY_ID=<your-registered-key-id> KEY_FILE=./a2a-key.json node a2a-client.mjs
// The key file holds YOUR Ed25519 seed ({"seed":"<base64 32 bytes>"}); it never leaves your machine.
import { createPrivateKey, sign, randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';

const BASE = process.env.A2A_BASE || 'https://ai100web.pages.dev';
const KEY_ID = process.env.KEY_ID;
const seed = Buffer.from(JSON.parse(readFileSync(process.env.KEY_FILE || 'a2a-key.json', 'utf8')).seed, 'base64');
const key = createPrivateKey({ key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]), format: 'der', type: 'pkcs8' });
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64');
const unb64 = (s) => (s ? JSON.parse(Buffer.from(s, 'base64').toString('utf8')) : null);

// Sign the exact bytes you send: Ed25519 over `${ts}.${rawBody}`, ts = unix seconds (±300 s).
// Limit: 5 POST/min per IP and per key id. On 429 wait Retry-After, then sign again (a re-sent signature is a replay).
async function post(path, body, extra = {}) {
  const raw = JSON.stringify(body);
  for (let attempt = 0; ; attempt++) {
    const ts = String(Math.floor(Date.now() / 1000));
    const sig = sign(null, Buffer.from(`${ts}.${raw}`), key).toString('base64');
    const r = await fetch(BASE + path, { method: 'POST', body: raw, headers: { 'content-type': 'application/json',
      'X-A2A-Key-Id': KEY_ID, 'X-A2A-Timestamp': ts, 'X-A2A-Signature': sig, ...extra } });
    if (r.status === 429 && attempt < 2) { await new Promise((ok) => setTimeout(ok, 1000 * (Number(r.headers.get('retry-after')) || 60))); continue; }
    return { status: r.status, headers: r.headers, json: await r.json().catch(() => null) };
  }
}
const message = (data) => ({ jsonrpc: '2.0', id: randomUUID(), method: 'message/send',
  params: { message: { role: 'user', kind: 'message', messageId: randomUUID(), parts: [{ kind: 'data', data }] } } });

// 1) Free skill → HTTP 200, task "completed".
const free = await post('/a2a', message({ skill: 'resolve-domain', params: { domain: 'a2aprotocol.ton' } }));
console.log('free:', free.status, free.json?.result?.status?.state);

// 2) Paid skill → HTTP 402 quote → attach the (simulated) proof → HTTP 200 with the real result.
const task = { skill: 'ai-summarize', maxSentences: 2,
  text: 'Agents discover the card. Every request is signed with Ed25519. Paid skills answer 402 with an x402 quote. The proof is attached on the retry. The skill runs only after the proof verifies.' };
const quote = await post('/a2a', message(task));
if (quote.status !== 402) throw new Error(`expected HTTP 402 quote, got ${quote.status}: ${JSON.stringify(quote.json)}`);
const ton = unb64(quote.headers.get('payment-required')).accepts.find((a) => a.asset === 'TON');
console.log('quote:', quote.status, ton.maxAmountRequired, 'nano to', ton.payTo, 'expires', ton.extra.expiry);
const proof = b64({ x402Version: 1, scheme: 'exact', network: 'ton-mainnet', payload: { simulated: true, nonce: ton.extra.nonce } });
const paid = await post('/a2a', message(task), { 'X-PAYMENT': proof });
const receipt = unb64(paid.headers.get('payment-response'));
const out = paid.json?.result?.artifacts?.[0]?.parts?.find((p) => p.kind === 'data')?.data;
console.log('paid:', paid.status, paid.json?.result?.status?.state, 'receipt', receipt?.receiptId, 'simulated', receipt?.simulated);
console.log('result:', out?.executed, out?.mode, out?.result?.summary);

// 3) The same proof again is refused (single-use nonce) → HTTP 402 replayed_nonce, nothing runs.
const again = await post('/a2a', message(task), { 'X-PAYMENT': proof });
console.log('replay:', again.status, again.json?.result?.metadata?.['x402.payment.error']);

// 4) Read-only MCP bridge (same signature scheme).
const tools = await post('/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list' });
console.log('mcp tools:', tools.status, tools.json?.result?.tools?.map((t) => t.name).join(', '));
```

## 7. MCP bridge (read-only)
`POST https://ai100web.pages.dev/mcp`, same signature; methods initialize, ping, tools/list, tools/call; tools: ton_get_balance, ton_dns_resolve.
```bash
BODY='{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
TS=$(date +%s); SIG=$(node sign.mjs "$TS" "$BODY")
curl -s https://ai100web.pages.dev/mcp -H "content-type: application/json" -H "accept: application/json" \
  -H "X-A2A-Key-Id: <your-key-id>" -H "X-A2A-Timestamp: $TS" -H "X-A2A-Signature: $SIG" --data-binary "$BODY"
# tools/call: {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ton_dns_resolve","arguments":{"domain":"a2aprotocol.ton"}}}
```

## 8. Errors
| HTTP | code | when |
|---|---|---|
| 401 | unauthorized / missing-signature-headers | signature headers missing |
| 401 | unauthorized / stale-timestamp | timestamp outside ±300s |
| 401 | unauthorized / invalid-signature | signature does not verify for that key id (also unregistered key ids) |
| 401 | unauthorized / replayed | the exact same signed request sent again |
| 403 | skill_not_declared | skill not in the Agent Card |
| 403 | skill_not_allowed_for_agent | a sub-agent key id called a skill outside its scope |
| 429 | rate_limited | over 5 requests/min per IP or per key id — see Retry-After |
| 402 | input-required (x402 quote) | paid skill without a payment proof |
| 402 | replayed_nonce | nonce already used — the skill does not run |
| 402 | expired_nonce | quote older than 300s |
| 402 | unknown_nonce | nonce never issued |
| 402 | invalid_payment_header | proof header not decodable, or X-PAYMENT and PAYMENT-SIGNATURE conflict |
| 402 | requester_mismatch · payer_mismatch · nonce_skill_mismatch | the nonce is bound to the requesting key id, the declared payer and the skill |
| 200 | state &quot;failed&quot; | payment verified but the skill could not run (e.g. invalid_input, upstream_unavailable) — receipt still returned |
| 401 | /mcp unauthorized | unsigned /mcp call |

JSON-RPC on /a2a: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32001 task not found. /mcp returns JSON-RPC errors with the HTTP status: -32001 Unauthorized (401, error.data.reason), -32005 Rate limited (429).
