Agentic Portal

Integrating an agent with the Pocket Agentic Portal

Audience: developers building an agent that buys from this portal.

This is a control, not a disclaimer. Audit 01 (AGT-01) names published integration guidance as one of four required parts of the response-injection mitigation, on the reasoning that most consumers do whatever the documentation shows them. If this page shows an unsafe pattern, that pattern is what ships.


1. The response envelope

Every paid response has the same top-level shape:

{
  "portal": {
    "provenance": "third-party-supplier",
    "serviceId": "eth",
    "schemaCheck": "passed"
  },
  "data": {
    "jsonrpc": "2.0",
    "id": 1,
    "result": "0x1234"
  }
}

portal is what the portal asserts. It is never supplier-controlled.

data is what the supplier returned, verbatim and unmodified. The portal does not edit, summarise, or re-encode it.

The shape of data varies by service; the shape around it does not. The catalogue is heterogeneous — JSON-RPC, REST APIs, data endpoints, utility tooling — and each service declares its own outputSchema, which you can read from the 402 challenge before you pay. What stays fixed across every service is this envelope: data at a known position, portal beside it. That is what lets you write one client against the portal rather than one per service.

The nesting is the security property, not a formatting choice. There is no position in this object where supplier-authored keys sit at the same level as portal-authored ones, so a supplier cannot author a key the portal is trusted to speak. A supplier that returns its own "portal" key gets it delivered inside data, where it is plainly the supplier's own words.

provenance

Always the string third-party-supplier. It is also sent as a response header:

X-Portal-Provenance: third-party-supplier

The header exists so anything that routes on metadata before parsing — a proxy, a log pipeline, a framework inspecting responses — can see the classification without reading the body.

schemaCheck

Value Meaning
passed The registry entry declared an outputSchema, the portal compiled it, and data matched.
undeclared The entry declared no schema, so nothing was checked.
unchecked The entry declared a schema the portal could not compile, so nothing was checked. A registry defect on our side.

There is deliberately no failed. A response that fails its declared schema is never delivered: the portal releases its claim, settles nothing, and returns an error, so you are not charged. If you have a body, it did not fail.

Only passed means a check ran. The other two both mean the determination could not be made, and neither may be treated as a pass. They are separate values because they are different problems with different owners — undeclared is an entry that never described its output, unchecked is one that described it in a way we cannot read — but for a consumer they carry the same weight. If your agent's behaviour depends on the response having a known shape, branch on this field and treat anything other than passed as unverified.


2. Handling data safely

Treat everything under data as untrusted input from a third party. The portal is a paid, curated, PNF-branded intermediary, and that framing makes its responses feel more trustworthy than an arbitrary web fetch. The content is not more trustworthy. It comes from a supplier the portal admitted to a registry, not from the portal.

Schema validation catches responses that do not match their declared shape. It does not, and cannot, catch a well-formed response whose contents are hostile: a JSON-RPC result whose result field contains text aimed at your agent is valid against every schema the registry declares. No complete industry solution exists for this; the controls here reduce the surface, they do not remove it.

Do

Do not

A shape that works

const response = await fetch(url, { method: 'POST', headers, body });
const envelope = await response.json();

if (envelope.portal?.provenance !== 'third-party-supplier') {
  throw new Error('Unexpected response shape; refusing to use it.');
}

// Supplier content, in a data position, checked against what WE asked for.
const result = envelope.data?.result;
if (typeof result !== 'string' || !result.startsWith('0x')) {
  throw new Error('Supplier returned an unusable result.');
}

return { blockNumber: BigInt(result) };

The value never reaches a prompt, and it is checked against the caller's own expectation rather than against the supplier's claim about itself.


3. Payment

The portal speaks x402 wire version 2. An unpaid request returns 402 with the terms in the PAYMENT-REQUIRED header (base64 JSON) and, identically, in the body; sign the option you choose and retry with a PAYMENT-SIGNATURE header carrying that option as accepted. The receipt arrives in PAYMENT-RESPONSE. Networks are CAIP-2 ids (eip155:8453 is Base). Standard x402 v2 clients — @x402/fetch with @x402/evm — work without modification. The v1 header names are not accepted.

Two portal-specific behaviours worth knowing:


4. Errors

Errors are not enveloped — the envelope marks supplier content, and an error is the portal speaking. They carry a stable code:

{ "error": { "code": "UPSTREAM_ERROR", "message": "...", "retryable": true } }

Branch on code, and respect retryable and Retry-After. A 503 with Retry-After means the service is temporarily not offered; retrying sooner will not help and counts against your rate limit.