# A syndicate behind your website — the A2A integration kit

Companion artifact to the project "Put a syndicate behind your own
website" at https://lyceumagents.com/projects/syndicate-behind-your-website/

The server is `scripts/a2a_server.ts` in the melchizedek-agents repo
(https://github.com/jhwadman/melchizedek-agents); the zero-dependency
client is `demo/a2a_demo.mjs`; the per-user memory rules are in
`lib/memory/README.md` §4. Section 4 of
https://lyceumagents.com/downloads/melchizedek-agent-setup.md is the
short form of this kit, with a paste-ready prompt for a coding agent.

---

## 1. .env — the server's side

```bash
A2A_SERVER_SECRET=<openssl rand -hex 32>   # every request carries it as a Bearer token
GOOGLE_GENAI_API_KEY=<your key>            # the server's own operations (memory extraction, embeddings)
# PUBLIC_URL=https://your-host.example     # set on deploy: forces the secret + DB hardening
# SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY # only for session-only / long-term syndicates
```

## 2. Start, and read the card

```bash
npm run start:a2a -- config/agents/examples/syndicate.yaml
curl http://localhost:4000/.well-known/agent-card.json
```

## 3. One message by hand

```bash
curl -X POST http://localhost:4000/a2a/jsonrpc \
  -H "Authorization: Bearer $A2A_SERVER_SECRET" \
  -H "X-API-Key: $GOOGLE_GENAI_API_KEY" \
  -H "X-Provider: google" \
  -H "X-User-Id: scratch-8f3a2c" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"message/send",
       "params":{"message":{"messageId":"req-001","role":"user",
                 "parts":[{"kind":"text","text":"In one sentence, what can you do?"}]},
                 "contextId":"scratch-thread-1"}}'
```

The three headers:
- `Authorization: Bearer` — proves the caller may talk to this server.
- `X-API-Key` — funds inference; the bill lands on the caller's account
  (BYOK). Funds the caller's OWN provider only; other providers' keys
  come from the server's environment.
- `X-User-Id` — the end user. `[A-Za-z0-9._-]{1,64}`, else 400.
  Namespaced beneath the key hash: `user_key = a2a-{keyhash}/{id}`.
- `X-Provider` — legacy; picks a default model only when the YAML omits
  `model:`. The YAML always wins.

Body: `messageId` per message; `contextId` per conversation (reuse it to
resume). The reply is the JSON-RPC `result` with the agent's text parts;
`[STATUS]` events name each delegation and tool call as it works.

Dynamic multi-agent routing: `POST /<agentId>/a2a/jsonrpc` serves a
syndicate by filename or registry id.

## 4. The one server-side function

```js
// server-side only — the secret and the key stay in this process's environment
const A2A_URL = process.env.A2A_URL; // e.g. https://your-server/patient_advocate/a2a/jsonrpc

export async function askAgent(userId, text, contextId) {
  const res = await fetch(A2A_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.A2A_SERVER_SECRET}`,
      'X-API-Key': process.env.BACKEND_GEMINI_KEY,
      'X-Provider': 'google',
      'X-User-Id': userId, // your app's opaque account id, set after login
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'message/send',
      params: {
        message: { messageId: crypto.randomUUID(), role: 'user', parts: [{ kind: 'text', text }] },
        contextId,
      },
    }),
  });
  if (!res.ok) throw new Error(`A2A ${res.status}`);
  return res.json();
}
```

Rules:
- the browser posts to YOUR route; it never sees the URL, the secret, or the key
- `userId` is the id your database assigned after authenticating the user — never an email, a name, or anything the visitor typed
- one `contextId` per conversation your app keeps

## 5. Erasure (right to be forgotten)

```bash
curl -X DELETE http://localhost:4000/memory \
  -H "Authorization: Bearer $A2A_SERVER_SECRET" \
  -H "X-API-Key: $GOOGLE_GENAI_API_KEY" \
  -H "X-User-Id: scratch-8f3a2c"
# → { "deleted": N }
```

Clears `adk_memory_facts` for that silo. Session transcripts
(`adk_sessions`) are a separate store; full erasure clears those rows too.

## 6. The silo test — keep it in your suite

1. user A tells the agent a fact; user B asks about it → B cannot recall it
2. user A, new `contextId` → A can recall it (long-term syndicates only)
3. erase A → A cannot recall it

## 7. The deploy checklist

- [ ] `A2A_SERVER_SECRET` set (the server refuses to start publicly without it)
- [ ] `PUBLIC_URL` set to the real address
- [ ] long-term memory: `db/hardening.sql` applied (the server refuses to start publicly without it; `ALLOW_UNHARDENED_DB=true` is a dev-only override)
- [ ] rate limit: 60 task submissions / 15 min / IP is the server's runaway ceiling — put real per-user limits in your backend
- [ ] cross-provider syndicates: the server's environment holds the non-caller providers' keys
- [ ] `dispatch:` syndicates run on the server only (the terminal runner compiles delegation)
- [ ] the erasure route wired to your account-deletion flow before the first real user
