Quickstart: Agent Data Plane — integrate your backend
中文版:快速上手:Agent 数据面。
This is the start-to-finish path for giving a Pouchy agent live, safe access
to your system: reading your authoritative state (Views), changing it
with user consent (Actions), and reacting when it changes (Events).
The worked example is an e-commerce backend — an Order view, a
refund_order action, an order.shipped event — but nothing here is
domain-specific: the same seven steps fit a game server, a clinic scheduler,
a logistics tracker, or an IoT fleet.
Works with any database — or none. The integration boundary is plain
HTTPS: you host two endpoints Pouchy calls (a View GET, an Action POST)
and you POST events to Pouchy. What sits behind your endpoints — Postgres,
Mongo, Redis, a spreadsheet, another API — is invisible to the platform by
construction. There are no connectors, no drivers, no query language: the
agent selects a declared capability by name and never authors a query.
Prerequisites: a Pouchy project (dashboard access), a backend reachable at
one HTTPS hostname you control (the audience), Node ≥18 if you use the
kit. Full wire contract: API Reference
(§Data capabilities). A runnable receiver lives in the repo's
examples/data-plane-receiver/.
On-prem / intranet backend? Pouchy never connects inward — you place a thin verify-first shim where Pouchy can reach it (cloud/DMZ host, reverse tunnel, or your API gateway's edge) and it talks to the internal system over your private network; DB credentials never leave the intranet. Deployment shapes and the security posture: Integration FAQ §9.
0) The mental model, 60 seconds
- You declare capabilities (dashboard → Data Capabilities, or headless via
the Admin API /
@pouchy_ai/admin-sdk). Published revisions are immutable — new content mints the next version, running sessions keep the revision they pinned. Publishing is never revocation; the per-agentdataflags and the per-capability disable bit are the live levers. - Pouchy signs every request it makes to you (POUCHY-ACTOR-V1). You verify first, then trust the verified subject — never the other way.
- Your HTTP answer is the outcome claim on Actions: 2xx = committed,
4xx = rejected, 5xx/timeout =
unknown(may have applied; resolved only by reconciliation, never blind retry).
1) Provision the signing secret — BEFORE anything else
Dashboard → Data Capabilities → signing card → Provision (or
POST /v1/projects/{id}/capabilities/signing {"action":"provision"}).
Record the pcsk_… plaintext now — it is shown exactly once.
Order matters. If no keys exist, the platform mints them on the first capability call (
test-readincluded) and auto-minted plaintext is never shown anywhere — provisioning afterwards answers409. If you hit that,rotate(fresh plaintext, once), switch your receiver,retire_previous.
2) Declare and publish the three capabilities
Dashboard → Data Capabilities → Publish, or headless (pchy_admin_… key):
import { createAdminClient } from '@pouchy_ai/admin-sdk'; // ≥0.12.0 — typed declarations
const admin = createAdminClient({ adminKey: process.env.POUCHY_ADMIN_KEY! });
await admin.publishCapability({
kind: 'view', name: 'Order',
description: 'One customer order: status, total, delivery ETA.',
audience: 'api.shop.example.com',
endpoint: 'https://api.shop.example.com/pouchy/order',
fields: ['orderId', 'status', 'total', 'eta'],
filters: ['orderId']
});
await admin.publishCapability({
kind: 'action', name: 'refund_order',
description: 'Refund one order to the original payment method.',
sensitivity: 'personal',
audience: 'api.shop.example.com',
endpoint: 'https://api.shop.example.com/pouchy/refund',
args: ['orderId', 'reason'],
idempotency: 'action_id',
reconcile: { endpoint: 'https://api.shop.example.com/pouchy/refund-status' }
});
await admin.publishCapability({
kind: 'event', name: 'order.shipped',
description: 'An order left the warehouse.',
source: 'shop-backend',
schemaVersion: 1,
subjectField: 'customerId'
});
Names are the agent's vocabulary (Order, not tbl_order_v2) — the
model plans with them. fields is an output allowlist: anything else your
endpoint returns is stripped before the agent sees it.
3) Implement the receiver (backend-kit does the wire)
npm install @pouchy_ai/backend-kit # ≥0.3.1 — Node ≥18, zero deps, typed
import {
verifyPouchyRequest, idempotentAction, reconcileFrom,
ActionRejected, verifyWebhookSignature
} from '@pouchy_ai/backend-kit';
const SECRETS = { [process.env.POUCHY_KID]: process.env.POUCHY_SIGNING_SECRET };
const AUDIENCE = 'api.shop.example.com';
// Every route: verify the EXACT raw bytes first, trust claims only after.
function verified(req, rawBody) {
return verifyPouchyRequest(
{ method: req.method, url: req.url, headers: req.headers, body: rawBody },
{ secrets: SECRETS, audience: AUDIENCE }
);
}
// GET /pouchy/order — the View. Filters arrive as query params (signed).
// → 200 flat JSON; Pouchy curates to the declared fields.
// POST /pouchy/refund — the Action. Body: {"actionId","action","args"}.
const refund = idempotentAction(store, async (args, ctx) => {
const order = await db.orders.get(args.orderId);
if (!order) throw new ActionRejected({ error: 'no such order' }, 422);
if (order.status !== 'paid') throw new ActionRejected({ error: 'not refundable' }, 409);
return await payments.refund(order, args.reason); // your durable receipt
});
// GET /pouchy/refund-status?actionId=… — reconciliation, three-state,
// answered from the SAME store (never by re-reading business state).
const refundStatus = reconcileFrom(store);
// POST /webhooks/pouchy — platform webhooks (agent.event_reply etc.):
const w = verifyWebhookSignature({
body: rawBody, header: req.headers['x-pouchy-signature'],
secret: process.env.POUCHY_WEBHOOK_SECRET });
Three rules the kit cannot enforce for you:
- Raw bytes. Every signature covers the exact wire bytes — mount these routes on the raw body, not a parsed-then-reserialized one.
- Refuse with
ActionRejected(a stored, replayed 4xx =rejected). Let unexpected crashes throw — a 500 honestly classifiesunknown. storeis yours — any backing that satisfies{ get(actionId), put(actionId, row) }, from a SQL table to a Map.
4) Prove it — headlessly, before any agent talks
await admin.testReadCapability('Order', { externalUserId: 'cust_42', filters: { orderId: 'o_1001' } });
// → the curated rows exactly as an agent would see them, plus the pinned revision
await admin.testActionCapability('refund_order', {
confirmDuplicates: true, // phases B/C intentionally re-deliver — real side effects
args: { orderId: 'o_test', reason: 'integration test' }
});
// → four phases: connectivity, duplicate same-intent, intent mismatch, reconcile.
// Protocol verdicts are reported SEPARATELY from outcomes — "Protocol: PASS,
// outcome: unknown" is a valid result, resolved by phase D.
const { executions } = await admin.listActionExecutions({ limit: 10 }); // read back what ran
Use a scratch order — the action test makes real deliveries.
5) Enable the agent
Dashboard → Agents → your agent → Data tab, or headless:
await admin.updateAgent(agentId, {
data: { enabled: true, actions: { enabled: true }, events: { enabled: true } }
});
enabled gives instances read_data + the pinned capability menu;
actions.enabled adds the confirm-gated run_action; events.enabled lets
your backend wake this agent. Every flag is live in the off direction —
flipping one kills that plane on the next turn of running sessions.
6) Emit events
import { PouchyClient } from '@pouchy_ai/backend-kit';
const pouchy = new PouchyClient({ projectId, token: process.env.POUCHY_SECRET_KEY });
await pouchy.emitEvent({
agentId, // omit → subscription fan-out
name: 'order.shipped',
eventId: `shipped:${orderId}`, // stable domain fact — Pouchy dedupes on it
data: { customerId, orderId }
});
An event is a signal, not a payload transport: data is mined for the
declared subjectField and discarded — the agent is told "order.shipped
occurred" and reads your Views for current facts. Without an
agent.event_reply webhook subscriber (step 3), accepted events defer
to the user's next conversation instead of waking one — subscribe the
webhook if you want proactive delivery. To require proof events come from
the declared source, publish with signing: 'required' and provision a
per-source pesk_ key (the kit signs when you pass source + sourceKeys).
7) Go-live checklist
- Signing secret provisioned FIRST and stored (step 1) — rotation plan noted
-
test-readreturns curated rows;test-actionfour phases green (step 4) - Refusals use
ActionRejected/ clean 4xx — never a catch-all 200 or 500 - Reconcile endpoint answers by actionId from the idempotency store
-
agent.event_replywebhook subscribed + verified (proactive Events) - Agent
dataflags on; automation (autoRun) only if you mean it - You know your two ground-truth readers: the Action journal and the
Event receipts (dashboard → Data Capabilities, or
listActionExecutions/listEventReceipts— same rows, admin-key mirrored)
Deeper: API Reference — Data capabilities (exact canonical strings, refusal semantics, automation two-key, source signing vectors), Capabilities Map (how the Data plane sits among the other capability planes).