This spec defines how two autonomous agents cooperate around a LetsPing-governed request using escrow metadata and signatures. It is intentionally small so different runtimes (Node, Python, etc.) can interoperate without tight coupling. Implementations that follow the envelope shape and HMAC rules below can participate in the same escrow chain regardless of language or framework.
1. Vocabulary
- Upstream agent — The agent that is currently in control of a request and wants to delegate work.
- Downstream agent — The agent that receives a delegated task and may further delegate or finalize it.
- Escrow envelope — The minimal, signed structure that ties a handoff to a specific LetsPing request and pair of agents.
2. Escrow envelope shape
Every LetsPing webhook can optionally include an escrow block. The envelope is the minimal structure that downstream agents and SaaS providers need to verify a handoff and optional payment mandates.
type EscrowEnvelope = {
id: string; // LetsPing request id
event: string; // e.g. "request.flagged", "request.approved"
data: any; // event payload (decision, risk info, etc.)
escrow?: {
mode: "none" | "handoff" | "finalized";
handoff_signature: string | null;
upstream_agent_id: string | null;
downstream_agent_id: string | null;
x402_mandate?: any; // optional AP2/x402 payment mandate attached to this handoff
ap2_mandate?: any; // optional AP2-style mandate metadata
};
};mode
"none"— No escrow active; normal one-agent flow."handoff"— Control is moving fromupstream_agent_id→downstream_agent_id."finalized"— The current agent is final and must not re-delegate this request under the same id.
3. Handoff signature
To make handoffs tamper-evident, LetsPing signs the escrow body with an HMAC. Downstream agents and storefronts must verify this signature before trusting upstream_agent_id or downstream_agent_id.
const base = {
id: event.id,
event: event.event,
data: event.data,
upstream_agent_id: event.escrow?.upstream_agent_id,
downstream_agent_id: event.escrow?.downstream_agent_id,
x402_mandate: event.escrow?.x402_mandate ?? null,
ap2_mandate: event.escrow?.ap2_mandate ?? null,
};
const handoff_signature = HMAC_SHA256(JSON.stringify(base), WEBHOOK_SIGNING_SECRET);All participating agents MUST treat WEBHOOK_SIGNING_SECRET as a shared secret between their orchestrator and LetsPing; recompute this HMAC and compare it to escrow.handoff_signature before trusting the handoff; and reject or log-and-quarantine any event with a mismatched signature. The SDK helper verifyEscrow(secret, eventBody) implements this for Node and Python.
4. Agent identity and call signatures
Every agent gets a logical id and secret: agent_id (stable identifier) and agent_secret (symmetric key known only to that agent and LetsPing). When an agent calls LetsPing /ingest, it signs the payload as follows.
const canonical = JSON.stringify({
project_id,
service,
action,
payload,
});
const agent_signature = HMAC_SHA256(canonical, agent_secret);The call body includes agent_id and agent_signature. LetsPing verifies this against its stored secret and marks agent_signature_valid on the request. Downstream services can use this flag to treat unsigned or invalid calls as suspect.
5. Handoff flow (happy path)
- Upstream agent calls LetsPing — Includes
agent_id,agent_signature, and an optionalescrowhint (e.g. targetdownstream_agent_id). - LetsPing processes guardrails and decisions — If an encoded handoff is required, it emits a webhook with
escrow.mode = "handoff"and a validhandoff_signature. - Downstream agent (or orchestrator) receives webhook — Uses
verifyEscrow(event, WEBHOOK_SIGNING_SECRET)to validate the handoff and confirms thatescrow.downstream_agent_idmatches its own declared id. - Downstream agent executes — Consumes
event.dataand any hydrated state snapshot. It may either call LetsPing again with its ownagent_id+agent_signature(continuing the escrow chain) or finalize and not re-delegate.
6. Chaining handoffs
Agents may form a chain A → B → C. The LetsPing SDK exposes helpers so each participant can verify and extend the chain without custom crypto.
import {
signAgentCall,
verifyEscrow,
chainHandoff,
} from "@letsping/sdk";- verifyEscrow(event, secret) — Recomputes the HMAC from the envelope and returns
trueif it matches. - signAgentCall(agentId, secret, call) — Produces
{ agent_id, agent_signature }to attach to/ingestcalls. - chainHandoff(previous, nextData, secret) — Produces
{ payload, escrow }for the next agent, tying its work to the original request id and a fresh HMAC.
Consumers MUST NOT modify previous.id when chaining; the LetsPing request id is the root of trust for the entire chain.
7. Failure and abuse handling
- If
verifyEscrowfails, the agent MUST treat the event as untrusted: do not execute downstream actions; log with high severity and optionally call a separate security pipeline. - If
agent_signature_valid === falseon the request, the dashboard and risk webhooks SHOULD treat the request as suspect and surface it to human operators.
8. Interop guarantees
If all participating agents obey (1) the escrow envelope shape, (2) the HMAC formulae above, and (3) the rule that id is stable along the chain, then:
- Different languages and frameworks can safely participate in the same escrow chain.
- Security teams can reconstruct the full chain of custody for any LetsPing request from audit logs and
risk_judgments.