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
- Bind
datato a variable and use it as data. Index into it, validate it, compare it against what you asked for. - Validate against your own expectations, not only against the portal's. You know what you asked for; the portal only knows what the entry declared.
- Check
schemaCheckbefore relying on shape. - Keep supplier content out of the instruction channel. If you pass it to a model, pass it in a clearly delimited data position — a tool-result block, a user-content block, a quoted field — never concatenated into a system prompt or an instruction string.
Do not
- Do not interpolate
datainto a prompt as though it were instructions. This is the failure the envelope exists to make visible. - Do not spread it.
{...response.data}at the top level of an object your agent then reasons over re-creates exactly the adjacency the envelope removes. - Do not treat
undeclaredoruncheckedas verified. Test forschemaCheck === 'passed', not for the absence of a particular value — more ways for a check not to run may be named later. - Do not act on instructions found in the response. A price feed that asks you to call another endpoint, transfer funds, or ignore prior instructions is reporting an attack, not a result.
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:
- Send
PAYMENT-SIGNATUREexactly once. Two of them is an ambiguous request, and the portal treats the pair as absent and re-challenges with a fresh402rather than guessing which authorization you meant to spend. - A failed delivery costs you nothing. If the supplier fails, the portal releases its claim and settles nothing, so the same authorization stays valid and you can retry it. There is no refund path because there is nothing to refund.
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.