LetspingLetsPing
← Docs

Agent-to-Agent Escrow Spec

How two agents safely hand off control using LetsPing escrow and cryptographic signatures.

v1 · Interoperable across Node, Python, and other runtimes

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 from upstream_agent_iddownstream_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)

  1. Upstream agent calls LetsPing — Includes agent_id, agent_signature, and an optional escrow hint (e.g. target downstream_agent_id).
  2. LetsPing processes guardrails and decisions — If an encoded handoff is required, it emits a webhook with escrow.mode = "handoff" and a valid handoff_signature.
  3. Downstream agent (or orchestrator) receives webhook — Uses verifyEscrow(event, WEBHOOK_SIGNING_SECRET) to validate the handoff and confirms that escrow.downstream_agent_id matches its own declared id.
  4. Downstream agent executes — Consumes event.data and any hydrated state snapshot. It may either call LetsPing again with its own agent_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 true if it matches.
  • signAgentCall(agentId, secret, call) — Produces { agent_id, agent_signature } to attach to /ingest calls.
  • 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 verifyEscrow fails, 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 === false on 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.

Further reading