Agentic Portal

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

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

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:

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:


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:

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:

  1. call_service without payment returns a tool result with isError: true and the x402 PaymentRequired in structuredContent (and, as JSON text, in content[0].text). These are the same terms /v1 quotes for that service.
  2. The client signs them and repeats the call with the payment in _meta["x402/payment"].
  3. 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.