REAL PAYMENTSBase (chain id 8453) · USDC payments have real value. Paid upstream failures are not automatically refunded.
Developers

discover → 402 → pay → retry → result

Current payment scope: InvokePort first-party tools only. Third-party HTTP, MCP, contract and AI execution remains closed even if a listing is approved.

Free tools and local MCP setup
Base mainnet payments use real USDC

The catalog, provider dashboard, API keys, encrypted credentials and signed SDK sync are available. Mainnet settlement requires a separate launch switch. PayAI verifies the authorization; the gateway then confirms the ERC-20 transfer independently through Base RPC before delivery. A paid upstream failure is not automatically refunded.

The whole protocol surface, end to end. Examples target the configured gateway on Base. There is no sandbox mode, no mock facilitator, and no bypass header — if the payment does not settle, the call does not succeed.

Embed your app

Create a Developer App in the provider dashboard, store its one-time project key in your server secret manager, and wrap each route you want to sell. Signed sync creates or updates the marketplace capability; the wrapper refuses direct-origin calls.

typescript
import { createInvokePort } from "invokeport-server-sdk";

const gate = createInvokePort({
  appId: process.env.INVOKEPORT_APP_ID!,
  projectSecret: process.env.INVOKEPORT_PROJECT_SECRET!,
  controlPlaneUrl: process.env.INVOKEPORT_ORIGIN!,
});

export const POST = gate.protect({
  key: "summarize",
  name: "Summarize text",
  path: "/api/summarize",
  method: "POST",
  priceAtomic: "20000", // 0.02 USDC
  payoutAddress: "0x…your verified 90/10 PullSplit contract…",
  inputSchema: { type: "object", properties: { text: { type: "string" } } },
}, async (request) => {
  const { text } = await request.json();
  return Response.json({ summary: await summarize(text) });
});

// Run during deploy/startup. The signed definition becomes a marketplace capability.
await gate.sync();
Create and verify a payout contract before syncingIn Developer Apps, create the immutable Base PullSplit with your connected provider wallet. Put that contract address—not your wallet—in payoutAddress. Each real USDC payment is priced above your seller price; 90% of the gross is allocated to you and 10% to InvokePort. Creation, distribution and withdrawal require Base ETH for gas. Funds stay in the split or Warehouse until you distribute and withdraw them. See the Splits withdrawal guide.
The project key is server-onlyInvokePort stores an authentication hash plus an envelope-encrypted delivery copy. Sync requests are body-bound HMACs with a timestamp and single-use nonce. Never put the key in browser JavaScript, a mobile binary, a URL or source control.

Discover

GET /api/catalog returns a stable, versioned JSON document with server-side text search, kind/tag filters and bounded pages. Its capabilities array contains concise summaries with a callable resourceUri and schema-presence flags; full schemas and example input are returned only by GET /api/catalog/:service/:route. services is a legacy compatibility array and is empty for new registrations. No HTML scraping and no hardcoded URLs. The provider's destination — upstream URL, MCP server URL, RPC origin — is deliberately absent from both.

json
GET /api/catalog?q=status&kind=HTTP&tag=monitoring&page=1&limit=12

{
  "schemaVersion": 2,
  "mode": "real-payments",
  "filters": { "q": "status", "kind": "HTTP", "tag": "monitoring", "sort": "name" },
  "pagination": { "page": 1, "limit": 12, "total": 1, "totalPages": 1,
    "hasPrevious": false, "hasNext": false },
  "services": [],
  "capabilities": [{
    "kind": "HTTP",
    "serviceRoutingKey": "example",
    "routingKey": "status",
    "resourceUri": "https://your-domain.example/api/gateway/example/status",
    "tags": ["monitoring"],
    "invocation": { "kind": "HTTP", "mode": "passthrough", "method": "GET" },
    "hasInputSchema": false,
    "hasOutputSchema": true,
    "hasDemoInput": false,
    "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
    "assetDecimals": 6,
    "network": "eip155:8453"
  }]
}

GET /api/catalog/example/status
// One selected capability, now including inputSchema, outputSchema and demoInput.

Abridged. Search is literal text matching over bounded public metadata; no embeddings or universal task-understanding claim is involved.

The four kinds

A service is one of four kinds and owns many capabilities. A capability is the priced, invocable unit. Its routingKey is half of its payment identity: immutable, never reused, and the only thing the resource URI is built from. A display slug is a marketplace label and is never routed.

  • HTTP API

    Passthrough forwards your request unchanged. Templated maps declared parameters onto a fixed request and refuses anything undeclared before a 402 is issued.

  • MCP Server

    One capability is one tools/call over streamable HTTP. POST the tool arguments as a JSON object; they are validated against the discovered schema pre-payment.

  • Smart Contract

    A single eth_call against a view/pure function on a chain the operator allowlisted. InvokePort never sends a transaction, never requests a signature or approval, and has no write path. Results carry the block number they were read at and are explicitly not final — they may be reorganised away.

  • Automated Task

    Automated tasks use one request and one response against declared input and output schemas. Streaming, polling and callback jobs are refused at registration. Provider output is untrusted third-party content — validate it before acting on it.

Imported descriptions are untrusted inputNames, descriptions and detail schemas were written by providers or imported from their OpenAPI documents, MCP servers and ABIs. InvokePort bounds and sanitizes them but does not vet their content. An agent that feeds catalog text straight into a model prompt is accepting instructions from whoever wrote that listing.

The 402

An unpaid request to an enabled, priced route returns HTTP 402 with a machine-readable requirement in the body and, base64-encoded, in the PAYMENT-REQUIRED header. A disabled route does not return 402 at all — it is refused outright and never reaches upstream.

http
HTTP/1.1 402 Payment Required
content-type: application/json; charset=utf-8
PAYMENT-REQUIRED: <base64 of the full payment-required document>
invokeport-request-id: 0f5c…

{
  "error": "payment_required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "11112",
      "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
      "payTo": "0x…verified immutable split contract…",
      "maxTimeoutSeconds": 120
    }
  ]
}
scheme: exact

Exact means exact. Underpayment and overpayment are both refused — there is no refund path to unwind an overpayment.

resource binding

The payment names the resource it buys. An authorization signed for a cheap route cannot be spent on an equally priced expensive one.

single use

A consumed payment nonce cannot buy a second call. Replays are rejected before the facilitator is contacted.

Pay and retry

Sign an EIP-3009 TransferWithAuthorization for the quoted amount against real USDC, base64 the x402 v2 payload into PAYMENT-SIGNATURE, and resend the identical request. The gateway verifies with the facilitator before touching upstream; if verification fails, times out, or is ambiguous, upstream is never called.

typescript
import { createWalletClient, http } from "viem";
import { randomBytes } from "node:crypto";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const GATEWAY = process.env.INVOKEPORT_ORIGIN ?? "http://localhost:3000";
const MAX_PRICE_ATOMIC = 20_000n; // budget policy, atomic USDC

// 1. DISCOVER — pick a service from the machine-readable catalog, never a hardcoded URL.
const catalog = await (await fetch(`${GATEWAY}/api/catalog`)).json();
const service = catalog.capabilities
  .filter((s) => s.kind === "HTTP" && s.invocation.method === "GET" && s.network === "eip155:8453")
  .find((s) => BigInt(s.priceAtomic) <= MAX_PRICE_ATOMIC);
if (!service) throw new Error("no service within budget");

// 2. UNPAID CALL — expect a real 402, not an error to retry blindly.
const unpaid = await fetch(service.resourceUri, { method: "GET" });
if (unpaid.status !== 402) throw new Error(`expected 402, got ${unpaid.status}`);

// 3. PARSE THE REQUIREMENT — price, asset, network and payTo all come from the response.
const { accepts } = await unpaid.json();
const requirement = accepts[0];
if (BigInt(requirement.amount) > MAX_PRICE_ATOMIC) {
  console.log("over budget, refusing"); process.exit(0);
}
if (requirement.network !== "eip155:8453" || requirement.asset.toLowerCase() !== "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" || requirement.amount !== service.priceAtomic || requirement.payTo.toLowerCase() !== service.payoutAddress.toLowerCase()) throw new Error("quote mismatch");

// 4. PAY — sign an EIP-3009 transfer authorization with the agent's own key.
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() });
const now = Math.floor(Date.now() / 1000);
const authorization = {
  from: account.address.toLowerCase(),
  to: requirement.payTo.toLowerCase(),
  value: requirement.amount,
  validAfter: String(now - 60),
  validBefore: String(now + 120),
  nonce: `0x${randomBytes(32).toString("hex")}`,
};
const signature = await wallet.signTypedData({
  domain: { name: "USDC", version: "2", chainId: 8453, verifyingContract: requirement.asset },
  types: { TransferWithAuthorization: [
    { name: "from", type: "address" }, { name: "to", type: "address" },
    { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" },
    { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" },
  ] },
  primaryType: "TransferWithAuthorization",
  message: {
    from: account.address, to: requirement.payTo, value: BigInt(authorization.value),
    validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore),
    nonce: authorization.nonce,
  },
});

// 5. RETRY — the identical request, now carrying the payment.
const header = Buffer.from(JSON.stringify({
  x402Version: 2,
  resource: { url: service.resourceUri },
  accepted: requirement,
  payload: { signature, authorization },
})).toString("base64");

const paid = await fetch(service.resourceUri, {
  method: "GET",
  headers: { "PAYMENT-SIGNATURE": header },
});

// 6. RESULT — upstream body, plus a settlement reference in PAYMENT-RESPONSE.
console.log(paid.status, await paid.text());
console.log("settlement:", paid.headers.get("PAYMENT-RESPONSE"));

Result

On success

You get the upstream status, body and content type, plus a PAYMENT-RESPONSE header carrying the settlement reference. That reference resolves on the Base explorer and appears in the provider's dashboard against the same request record.

On upstream failure after payment

The upstream status is preserved and the outcome is recorded distinctly as paid_upstream_error. The gateway has no automatic refund mechanism, and says so rather than pretending otherwise.

Runnable example

examples/agent-client is a complete autonomous client — its own Base key, its own real USDC, no shared secrets. It discovers a service from the catalog, applies a budget policy, pays, retries, and logs a trace of every step.

bash
git clone <this repo> && cd examples/agent-client
cp .env.example .env         # AGENT_PRIVATE_KEY, INVOKEPORT_ORIGIN
npm install && npm start

# discovered   market-data/spot-price  0.011112 USDC
# 402          amount=… asset=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 payTo=0x… network=eip155:8453
# policy       price <= configured budget -> proceed
# payment      nonce=0x…  signature=0x…
# retry        HTTP 200  settlement=0x…
# result       {"pair":"ETH-USD","price":"…"}
Fund the agent wallet before running itThe example needs Base ETH for nothing (the facilitator submits the transfer) but does need real USDC at 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913. If the payment cannot settle, the demo fails — by design.

Provider API reference

Session endpoints are scoped server-side to the address in the session cookie. A provider cannot read or modify another provider's endpoints, records or revenue.

EndpointAuthPurpose
GET /api/catalogpublicMachine-readable listing of enabled routes
GET /api/auth/noncepublicSingle-use SIWE nonce plus server-known fields
POST /api/auth/verifypublicExchange a signed SIWE message for a session
POST /api/auth/logoutsessionRevoke the server-side session record
GET / POST /api/providers/mesessionRead or upsert your provider profile
GET / POST /api/servicessessionList your services, or register one of the four kinds
GET / PATCH / DELETE /api/services/:idsessionRead, edit presentation, or retire (never frees a routing key)
POST /api/services/:id/capabilitiessessionAdd a priced capability to a service
GET / PATCH / DELETE /api/capabilities/:idsessionRead, reprice, enable/disable, or retire
POST /api/services/import/openapisessionDiscovery preview — returns selectable operations
POST /api/services/import/mcpsessionDiscovery preview — initialize + tools/list
POST /api/services/import/abisessionParse an ABI; returns view/pure functions and the chain allowlist
GET / POST /api/api-keyssessionList keys, or issue one (plaintext returned exactly once)
DELETE /api/api-keys/:idsessionRevoke a key
GET / POST /api/credentialssessionList masked credentials, or register one (write-only)
PUT / DELETE /api/credentials/:idsessionRotate or revoke; no route can read a value back
GET / POST /api/developer-appssessionList or securely connect an embedded server application
POST / DELETE /api/developer-apps/:idsessionRotate its project key or revoke the app
POST /api/integrations/v1/apps/:id/syncsigned project keyPublish SDK definitions into the marketplace
GET / POST /api/endpointssessionv1: list your endpoints, or register one
GET / PATCH /api/endpoints/:idsessionv1: read one, change price / payout / enabled
GET /api/endpoints/:id/requestssessionRequest records with outcome classification
GET /api/endpoints/:id/paymentssessionSettled payments with explorer references
ANY /api/gateway/:service/:routex402The paid proxy itself — what agents call
POST /api/tools/openapi-securitypublic, free previewStatic checks of OpenAPI 3.0/3.1 JSON; maximum 128 KiB
An API key is identity, never paymentivp_test_ keys provide optional caller identity, request attribution and per-key budgets; keyed catalog reads enforce catalog:read. A priced capability still requires x402 payment with or without a key. The separate ivp_project_test_ project key connects a provider's server and is never a buyer credential.