Integrating an agent with the Pocket Agentic Portal
How an agent calls and pays for any service on the Pocket Network Agentic Portal: the response envelope, handling supplier content safely, paying per call with x402 or MPP, the error codes, and the MCP server. For 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
Every endpoint is paid per call, with no account or API key, over x402 or
MPP. An unpaid request returns 402 carrying the terms for each; pay with
whichever your client supports.
x402
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.
MPP
The portal also speaks MPP, the Machine Payments Protocol
(mpp.dev), on Tempo in USDC.e (eip155:4217). When it is
offered, the same 402 carries a WWW-Authenticate: Payment challenge with
method tempo and intent charge; its request names the amount, the token,
the recipient and the modes, pull and push. Retry with
Authorization: Payment <credential>. The receipt arrives in Payment-Receipt.
A challenge lives for twenty minutes. mppx (mppx/client, with its tempo
method) is a working client.
The two modes settle differently, and it matters when a call fails:
- Pull: you sign the transfer and send it unbroadcast. The portal verifies it, serves the call, and only then broadcasts it, so a failed delivery moves no money.
- Push: you broadcast the transfer yourself and send its hash. The money has
already moved, so a failed delivery stays redeemable for 15 minutes:
retry the same service with the same credential and it is served without
paying again. Nothing is refunded. Your client has to make that retry itself,
because
mppx's client does not.
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.
UPSTREAM_REJECTED (400, not retryable) means the service refused your
request body. It is a failed delivery like any other: a pull payment does not
settle, and a push payment stays redeemable. Sending the same body again gets
the same answer, so fix it against the service's inputSchema and example
first.
5. From an MCP client
Claude, Cursor and other MCP clients can call and pay for these services through
@pocket-network/agentic-portal-mcp, a local MCP server listed in the
official MCP Registry as network.pocket/agentic-portal-mcp
(npm,
source). It runs on your
machine, holds the paying wallet's key there, and pays this portal over x402 on
Base. No client pays for tools natively yet, which is why the key sits in a
local server rather than with the portal.
It has three tools:
| Tool | What it does | Costs |
|---|---|---|
search_services |
Search the catalogue by words and category. | free |
describe_service |
One service: price, operations, schemas and a real captured example. | free |
call_service |
Call a service and pay its price. | the service's price |
call_service returns the receipt and this portal's response envelope,
unchanged, so everything in section 2 applies: data is third-party content,
never instructions.
Add it to the client's MCP configuration (claude_desktop_config.json,
.cursor/mcp.json, or .mcp.json for Claude Code):
{
"mcpServers": {
"pocket-network": {
"command": "npx",
"args": ["-y", "@pocket-network/agentic-portal-mcp"],
"env": {
"POCKET_PRIVATE_KEY": "0x…",
"POCKET_MAX_TOTAL_ATOMIC": "1000000"
}
}
}
}
Settings go in that env block: desktop clients start MCP servers without your
shell's environment. With no key, only the free tools work.
It signs nothing unless every limit allows it, and it checks them before signing:
POCKET_MAX_TOTAL_ATOMIC: the most it signs for in total while it runs, in atomic units (USDC has 6 decimals:1000000is $1.00). Required to pay. Spend is counted when a payment is signed, not when an answer arrives.POCKET_MAX_PER_CALL_ATOMIC: the most for one call. Default: the highest price in the catalogue.POCKET_NETWORK: the only network it signs on. Default: Base,eip155:8453.POCKET_QUOTE_ONLY=true: return the seller's terms and never pay.
A call is also refused before paying when the service is unknown or not serving, or when the path or JSON-RPC method is not one the service lists.
Do not rely on the client to ask before paying. In testing, one client's model asked for a go-ahead and another paid without asking. The limits above are the controls that hold either way. Use a wallet made for this, holding only what you are willing to spend: its key sits in a plain-text configuration file.
The hosted endpoint
The same three tools are served at https://agent.pocket.network/mcp
(Streamable HTTP, JSON responses, no session), for a client that pays x402
itself and keeps its own key. It is listed with the npm package in the MCP
Registry entry network.pocket/agentic-portal-mcp.
Paying there is x402 inside MCP:
call_servicewithout payment returns a tool result withisError: trueand the x402PaymentRequiredinstructuredContent(and, as JSON text, incontent[0].text). These are the same terms/v1quotes for that service.- The client signs them and repeats the call with the payment in
_meta["x402/payment"]. - The result is this portal's envelope, and the receipt is in
_meta["x402/payment-response"]. If settlement fails, the result is the payment error alone, never the data.
@x402/mcp's createx402MCPClient is a working client. Free requests (the
handshake, the tool list, search, describe, an unpaid call_service) are
limited per IP; a paid call is not. A call to an operation the service does not
list is refused before it is priced. Browsers are accepted only from this
portal's own origin.