Documentation

AgentPort API Reference

AgentPort wraps any Hermes agent with authentication, rate limiting, cryptographic receipts, and on-chain payment routing. This page documents every endpoint, environment variable, and internal algorithm.

Introduction

AgentPort is a self-hosted API gateway purpose-built for Hermes agents running in the AgentPort ecosystem. It sits between the world and your agent, handling four concerns so your agent doesn't have to:

  • AuthenticationAdmin secrets for operators, API keys for callers. Keys are scoped per-agent and revocable without redeploying.
  • Rate limitingPer-key sliding-window rate limiter with in-memory state. Limits exposed via standard response headers.
  • Cryptographic receiptsEvery call generates a tamper-evident SHA-256 receipt binding call ID, agent ID, timestamp, and full request/response hashes. Returned in the HTTP response header; verifiable without trusting the gateway.
  • Payment routingThree progressive layers: free access, API-key gated access, and trustless x402 on-chain AGNP micropayments on Base. No rewrites between layers.

The gateway is a Fastify application (Node.js 20+) with a libSQL/SQLite database. In production it deploys as a Vercel serverless function or as a long-running process on Railway or any VPS.

Architecture

A single gateway deployment manages multiple Hermes agents. The call flow is:

bash
Caller → AgentPort Gateway → Hermes Agent
         ↓
   [auth check]         validates x-api-key or x-payment-tx
   [rate limit]         sliding-window counter per key
   [proxy call]         POST hermesUrl/hermesPath with caller body
   [receipt gen]        sha256(callId:agentId:ts:reqHash:resHash)
   [log to db]          INSERT INTO calls(...)
         ↓
   Response + X-AgentPort-Receipt header

The database stores three tables: agents, api_keys, and calls. Rate limiting is kept in-memory (not persisted); it resets on restart. This is intentional — a restarted gateway should not carry over old counters from a previous window.

Data model

bash
agents
  id            TEXT  PK        8-char UUID prefix
  name          TEXT
  hermes_url    TEXT            base URL of the upstream Hermes instance
  hermes_path   TEXT  DEFAULT /api/chat
  description   TEXT
  is_public     INT   0|1       listed in the public registry
  pricing_mode  TEXT  free|per_call
  price_per_call TEXT           AGNP amount string
  created_at    INT             Unix ms

api_keys
  key           TEXT  PK        "ap_" + 32 random base64url chars
  agent_id      TEXT            FK → agents.id
  label         TEXT
  rate_limit_per_hour  INT
  is_active     INT   0|1       0 = revoked (soft-delete)
  created_at    INT

calls
  id            TEXT  PK        full UUID
  agent_id      TEXT
  api_key       TEXT  nullable
  request_hash  TEXT            sha256 of raw request body
  response_hash TEXT            sha256 of upstream response body
  receipt_hash  TEXT            sha256 of the receipt preimage
  status_code   INT
  duration_ms   INT
  payment_tx    TEXT            x402 transaction hash if applicable
  created_at    INT

Quick Start

Three commands from zero to a callable agent:

bash
# 1. Register your Hermes agent
curl -X POST https://your-gateway.vercel.app/admin/agents \
  -H "x-admin-secret: $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "name":         "my-analyst",
    "hermesUrl":    "https://my-hermes.up.railway.app",
    "hermesPath":   "/api/chat",
    "description":  "Financial data analyst",
    "isPublic":     true,
    "pricingMode":  "per_call",
    "pricePerCall": "0.01"
  }'
# → { "agentId": "a1b2c3d4", "endpoint": "/v1/agents/a1b2c3d4/run" }

# 2. Issue an API key for a caller
curl -X POST https://your-gateway.vercel.app/admin/agents/a1b2c3d4/keys \
  -H "x-admin-secret: $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "label": "partner-1", "rateLimitPerHour": 500 }'
# → { "key": "ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxx", ... }

# 3. Call the agent
curl -X POST https://your-gateway.vercel.app/v1/agents/a1b2c3d4/run \
  -H "x-api-key: ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "messages": [{ "role": "user", "content": "Analyze AGNP liquidity" }] }'
# ← 200 OK
# X-AgentPort-Receipt: eyJ2IjoiMSIsImNhbGxJZCI6...
# X-AgentPort-Duration-Ms: 284
# X-RateLimit-Remaining: 499

Environment Variables

These apply to the gateway (apps/gateway/.env).

VariableDefaultDescription
PORT4000TCP port the Fastify server binds to.
ADMIN_SECRETdev-secretSecret required on all /admin/* routes via the x-admin-secret header. Change this in production.
DB_PATH./agentport.dbPath to the local SQLite database file. Ignored when TURSO_DB_URL is set.
TURSO_DB_URLlibsql:// URL for a remote Turso database. When set, overrides DB_PATH. Required for Vercel (ephemeral FS).
TURSO_AUTH_TOKENAuth token for the Turso database. Required when TURSO_DB_URL is set.
REGISTRY_URLURL of the central AgentPort registry. When set, newly public agents send a beacon POST on registration.
REGISTRY_SECRETShared secret for authenticating beacon pushes to the registry.
GATEWAY_PUBLIC_URLPublic base URL of this gateway, included in beacon payloads so the registry knows how to reach it.
PAYMENT_WALLETOperator wallet address on Base. Callers send AGNP here for x402 payments.
BASE_RPC_URLhttps://mainnet.base.orgBase chain RPC endpoint used to verify x402 payment transactions.
NODE_ENVdevelopmentSet to production in production deployments. Affects log level and cookie security.

These apply to the website (apps/website/.env.local).

VariableDefaultDescription
GATEWAY_URLhttp://localhost:4000URL the website server-side API routes use to reach the gateway. Should point to your deployed gateway in production.
ADMIN_SECRETdev-secretMust match the gateway's ADMIN_SECRET. Used server-side only (not exposed to the browser).
DASHBOARD_PASSWORDchangemePassword to access the dashboard. Hashed into a session token via HMAC-SHA256.
REGISTRY_SECRETchange-meUsed by the website's /api/registry endpoint to authenticate incoming beacon pushes from gateways.
NEXT_PUBLIC_SITE_URLhttp://localhost:3002Canonical URL of the website. Used for Open Graph and canonical link tags.
Warning:Never commit ADMIN_SECRET or DASHBOARD_PASSWORD to version control. Add .env and .env.local to your .gitignore.

Deployment

Vercel (recommended)

The gateway ships as a Vercel serverless function via apps/gateway/api/index.ts. Because Vercel's filesystem is ephemeral, you must use a remote Turso database.

bash
# 1. Create a Turso database
turso db create agentport

# 2. Get credentials
turso db show agentport          # → TURSO_DB_URL
turso db tokens create agentport # → TURSO_AUTH_TOKEN

# 3. Set env vars in Vercel
vercel env add TURSO_DB_URL
vercel env add TURSO_AUTH_TOKEN
vercel env add ADMIN_SECRET
vercel env add GATEWAY_PUBLIC_URL  # e.g. https://agentport-xyz.vercel.app
vercel env add PAYMENT_WALLET      # your Base wallet address

# 4. Deploy
vercel --prod
Note:Serverless functions on Vercel restart frequently. The rate limiter is in-memory and will reset between invocations. For production rate limiting, consider replacing the in-memory store with a Redis/Upstash adapter.

Railway

Railway runs the gateway as a persistent Node.js process — the rate limiter stays warm between requests and you can use a local SQLite file (though a Turso remote DB is still recommended for reliability).

bash
# railway.toml (in apps/gateway/)
[build]
  builder    = "NIXPACKS"
  buildCommand = "pnpm build"

[deploy]
  startCommand = "node dist/index.js"
  healthcheckPath = "/health"

# Set env vars in the Railway dashboard:
# PORT, ADMIN_SECRET, DB_PATH (or TURSO_*), GATEWAY_PUBLIC_URL

Self-hosted

Any server with Node.js 20+ and a process manager (PM2, systemd) works.

bash
cd apps/gateway
pnpm build                     # compiles TypeScript → dist/
node dist/index.js             # or: pm2 start dist/index.js --name agentport

# With a .env file:
# tsx watch --env-file=.env src/index.ts   (dev)
# node dist/index.js                        (production, after build)

Authentication

Admin secret

All /admin/* routes require the x-admin-secret header matching the gateway's ADMIN_SECRET env var. This is your operator credential — keep it out of client-side code.

bash
# Correct
curl -H "x-admin-secret: $ADMIN_SECRET" https://your-gateway.vercel.app/admin/stats

# Missing → 401
{ "error": "Unauthorized" }

API keys

API keys are issued per-agent via POST /admin/agents/:id/keys. They are prefixed ap_ followed by 32 bytes of cryptographically random data encoded as base64url (totaling ~46 characters).

Callers include the key in the x-api-key request header. The gateway looks up the key in the database, verifies it is active and scoped to the requested agent, then proceeds to rate-limit checking. Keys that are revoked (soft-deleted via DELETE /admin/keys/:key) immediately stop working — no redeploy needed.

bash
# Call a paid agent with an API key
curl -X POST https://your-gateway.vercel.app/v1/agents/:agentId/run \
  -H "x-api-key: ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "messages": [{ "role": "user", "content": "Hello" }] }'

Dashboard session

The website dashboard uses a separate password (DASHBOARD_PASSWORD) that produces a signed session cookie via POST /api/auth/login. The cookie is HTTP-only, SameSite=lax, and valid for 7 days. Sessions are signed with HMAC-SHA256 using ADMIN_SECRET as the key.

Rate Limiting

AgentPort uses a sliding-window in-memory rate limiter scoped per API key. Each key has a rateLimitPerHour budget set at creation time.

How it works

The limiter stores a list of call timestamps for each key. On every request, it drops timestamps older than one hour, counts the remainder against the limit, then appends the current timestamp. This gives a true sliding window rather than a fixed-bucket approximation.

Response headers

bash
X-RateLimit-Limit:     500        # your key's per-hour budget
X-RateLimit-Remaining: 497        # calls left in the current window
X-RateLimit-Reset:     1716048000 # Unix timestamp (seconds) when the window fully resets

Exceeded

bash
HTTP 429 Too Many Requests
X-RateLimit-Limit:     500
X-RateLimit-Remaining: 0
X-RateLimit-Reset:     1716048000

{ "error": "Rate limit exceeded" }
Note:The in-memory store resets on gateway restart. On Vercel (serverless), each cold start resets counters. For hard rate-limit enforcement in serverless environments, replace the store with Redis or Upstash.

Receipts

Every successful agent call produces a cryptographic receipt — a tamper-evident proof of the call's content, timing, and identity. Receipts are returned in the X-AgentPort-Receipt response header as a base64url-encoded JSON object.

Receipt schema

bash
{
  "v":            "1",             // receipt version
  "callId":       "uuid-v4",       // unique call identifier
  "agentId":      "a1b2c3d4",      // 8-char agent ID
  "timestamp":    1716048000000,   // Unix ms when call was processed
  "requestHash":  "sha256hex...",  // sha256 of the raw JSON request body
  "responseHash": "sha256hex...",  // sha256 of the upstream response body
  "receiptHash":  "sha256hex..."   // binding hash — see algorithm below
}

Receipt algorithm

The receiptHash is computed as:

bash
receiptHash = sha256(
  callId + ":" + agentId + ":" + timestamp + ":" + requestHash + ":" + responseHash
)

All five fields are concatenated with : separators. The timestamp is the raw Unix millisecond integer (no formatting). Hashes are lowercase hex strings. Changing any field — even one byte of the request or response — produces a different receiptHash.

Independent verification

You can verify any receipt without trusting the gateway:

bash
// TypeScript
import { createHash } from 'crypto'
import { Buffer } from 'buffer'

function verifyReceipt(encoded: string): boolean {
  const r = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'))
  const expected = createHash('sha256')
    .update(`${r.callId}:${r.agentId}:${r.timestamp}:${r.requestHash}:${r.responseHash}`)
    .digest('hex')
  return expected === r.receiptHash
}
bash
# Python
import hashlib, base64, json

def verify_receipt(encoded: str) -> bool:
    r = json.loads(base64.urlsafe_b64decode(encoded + "=="))
    preimage = f"{r['callId']}:{r['agentId']}:{r['timestamp']}:{r['requestHash']}:{r['responseHash']}"
    expected = hashlib.sha256(preimage.encode()).hexdigest()
    return expected == r['receiptHash']

Decoding the header

bash
# The receipt is in the X-AgentPort-Receipt response header
RECEIPT=$(curl -si ... | grep -i "x-agentport-receipt" | awk '{print $2}')

# Decode it
echo $RECEIPT | python3 -c "
import sys, base64, json
raw = sys.stdin.read().strip()
data = json.loads(base64.urlsafe_b64decode(raw + '=='))
print(json.dumps(data, indent=2))
"

Payments

AgentPort implements three progressive payment modes. Operators select the mode per-agent at registration time.

ModeStatusAuth headerUse case
freeLiveNone requiredPublic demos, open APIs
per_callLivex-api-key: ap_...Private or partner access
x402Livex-payment-tx: 0x...Trustless micropayments
agnpLiveAGNP token on BaseNative ecosystem payments
usdcLiveUSDC on Base via x402Stable settlement

402 Payment Required flow

When a caller hits a non-free agent without valid auth, the gateway returns HTTP 402:

bash
HTTP/1.1 402 Payment Required
X-Payment-Required:  true
X-Price:             0.01 AGNP
X-Payment-Methods:   api-key,x402
X-Payment-Address:   0x95ccfd2b81a9667b0cc979992632f98fc853eba3
X-Payment-Token:     0x95ccfd2b81a9667b0cc979992632f98fc853eba3
X-Payment-Chain:     base

{
  "error": "Payment required",
  "pricing": {
    "mode": "per_call",
    "pricePerCall": "0.01",
    "currency": "AGNP",
    "methods": ["api-key", "x402"],
    "paymentAddress": "0x...",
    "tokenContract": "0x95ccfd2b81a9667b0cc979992632f98fc853eba3",
    "chain": "base",
    "chainId": 8453
  }
}

The caller can then send a AGNP transfer on Base and retry with the transaction hash in the x-payment-tx header. The gateway verifies the transaction on-chain before proxying the call.

Public API

These endpoints do not require authentication.

Run an agent

POST/v1/agents/:agentId/run

Proxies a call to the upstream Hermes instance. Returns the agent's response plus receipt headers.

Headers

FieldTypeRequiredDescription
x-api-keystringconditionalRequired for agents with pricingMode ≠ free. Format: ap_...
x-payment-txstringconditionalx402: on-chain transaction hash of a AGNP transfer to the operator wallet.
Content-TypestringyesMust be application/json.

Body

Forwarded verbatim to the Hermes agent. Typically:

bash
{
  "messages": [
    { "role": "user",      "content": "What is the price of ETH?" },
    { "role": "assistant", "content": "..." },   // optional — for multi-turn
    { "role": "user",      "content": "And BTC?" }
  ]
}

Success response

bash
HTTP/1.1 200 OK
X-AgentPort-Receipt:      eyJ2IjoiMSIsImNhbGxJZCI...
X-AgentPort-Call-Id:      a3f9b1c2-fd1e-4df2-930b-1a2b3c4d5e6f
X-AgentPort-Duration-Ms:  284
X-RateLimit-Limit:        500
X-RateLimit-Remaining:    497
X-RateLimit-Reset:        1716048000

// Body is the raw Hermes response, passed through unchanged

Error responses

bash
404  { "error": "Agent not found" }
402  { "error": "Payment required", "pricing": { ... } }
429  { "error": "Rate limit exceeded" }
502  Upstream Hermes call failed (gateway-level error)

List public agents

GET/v1/agents

Returns all agents where is_public = 1, ordered by registration date descending.

bash
GET /v1/agents

{
  "agents": [
    {
      "id":            "a1b2c3d4",
      "name":          "My Analyst",
      "description":   "Financial data analyst",
      "pricing_mode":  "per_call",
      "price_per_call":"0.01",
      "created_at":    1716048000000
    }
  ]
}

List receipts for an agent

GET/v1/agents/:agentId/receipts

Returns paginated call records for a given agent. No auth required — public call history.

Query params

FieldTypeRequiredDescription
limitnumbernoMax records to return. Default 20, maximum 100.
pagenumbernoPage number (1-indexed). Default 1.
bash
GET /v1/agents/a1b2c3d4/receipts?limit=20&page=1

{
  "receipts": [
    {
      "id":            "uuid",
      "receipt_hash":  "sha256hex...",
      "request_hash":  "sha256hex...",
      "response_hash": "sha256hex...",
      "status_code":   200,
      "duration_ms":   284,
      "created_at":    1716048000000
    }
  ],
  "total": 142,
  "page":  1,
  "limit": 20
}

Verify a receipt

POST/v1/receipts/verify

Verifies the integrity of any receipt returned by the gateway. Checks both the hash chain and the database record.

Body

FieldTypeRequiredDescription
receiptstringyesBase64url-encoded receipt string from the X-AgentPort-Receipt header.
bash
POST /v1/receipts/verify
Content-Type: application/json

{ "receipt": "eyJ2IjoiMSIsImNhbGxJZCI6..." }

// Response
{
  "valid": true,
  "receipt": {
    "v":            "1",
    "callId":       "a3f9b1c2-...",
    "agentId":      "a1b2c3d4",
    "timestamp":    1716048000000,
    "requestHash":  "3f8a2b9c...",
    "responseHash": "9c2e5d8b...",
    "receiptHash":  "1a4b7e2c..."
  },
  "onChain": {
    "found":      true,
    "callId":     "a3f9b1c2-...",
    "statusCode": 200,
    "durationMs": 284,
    "timestamp":  1716048000000
  }
}

// Invalid receipt
{ "valid": false, "receipt": { ... }, "onChain": { "found": false } }

Health check

GET/health
bash
GET /health

{ "status": "ok", "version": "0.1.0", "name": "AgentPort" }

Admin API

All admin endpoints require x-admin-secret: {your-secret}.

Register an agent

POST/admin/agents

Body

FieldTypeRequiredDescription
namestringyesDisplay name. Must be non-empty.
hermesUrlstringyesBase URL of the upstream Hermes instance (no trailing slash).
hermesPathstringnoRequest path on the Hermes instance. Default: /api/chat.
descriptionstringnoHuman-readable description. Shown in the registry.
isPublicbooleannoWhether to list in the public registry. Default: false.
pricingModestringno"free" or "per_call". Default: "free".
pricePerCallstringnoAGNP amount per call (decimal string). Required when pricingMode = "per_call".
bash
POST /admin/agents
x-admin-secret: <secret>

{
  "name":          "My Analyst",
  "hermesUrl":     "https://my-hermes.up.railway.app",
  "hermesPath":    "/api/chat",
  "description":   "A helpful financial analyst",
  "isPublic":      true,
  "pricingMode":   "per_call",
  "pricePerCall":  "0.01"
}

// 201 Created
{
  "agentId":  "a1b2c3d4",
  "name":     "My Analyst",
  "endpoint": "/v1/agents/a1b2c3d4/run"
}

List all agents

GET/admin/agents

Returns all registered agents (public and private), ordered by creation date descending.

bash
GET /admin/agents
x-admin-secret: <secret>

{
  "agents": [
    {
      "id":             "a1b2c3d4",
      "name":           "My Analyst",
      "hermes_url":     "https://my-hermes.up.railway.app",
      "hermes_path":    "/api/chat",
      "description":    "...",
      "is_public":      1,
      "pricing_mode":   "per_call",
      "price_per_call": "0.01",
      "created_at":     1716048000000
    }
  ]
}

Update an agent

PATCH/admin/agents/:agentId

Partial update — only fields present in the body are changed.

Body (all fields optional)

FieldTypeRequiredDescription
namestringnoNew display name.
hermesUrlstringnoNew upstream URL.
hermesPathstringnoNew upstream path.
descriptionstringnoNew description.
isPublicbooleannoToggle public visibility.
pricingModestringno"free" or "per_call".
pricePerCallstringnoNew price (decimal string).
bash
PATCH /admin/agents/a1b2c3d4
x-admin-secret: <secret>

{ "pricePerCall": "0.05", "description": "Updated description" }

// 200 OK
{ "updated": true }

Delete an agent

DELETE/admin/agents/:agentId

Permanently deletes the agent and all its API keys. This cannot be undone.

bash
DELETE /admin/agents/a1b2c3d4
x-admin-secret: <secret>

// 200 OK
{ "deleted": true }

Issue an API key

POST/admin/agents/:agentId/keys

Creates a new API key scoped to the given agent. The key is returned once — store it securely.

Body

FieldTypeRequiredDescription
labelstringnoHuman label for this key (e.g. "production", "user-42").
rateLimitPerHournumbernoMax calls per hour for this key. Default: 100.
bash
POST /admin/agents/a1b2c3d4/keys
x-admin-secret: <secret>

{ "label": "partner-1", "rateLimitPerHour": 500 }

// 201 Created
{
  "key":              "ap_ojE8S1SzH6i_rrnef7vQ-gmeR-MTd8i8xxxxxxxxxx",
  "agentId":          "a1b2c3d4",
  "label":            "partner-1",
  "rateLimitPerHour": 500
}
Warning:The full key value is only returned once, at creation. It is stored as plaintext in the database (no hashing), so protect your database accordingly.

List keys for an agent

GET/admin/agents/:agentId/keys
bash
GET /admin/agents/a1b2c3d4/keys
x-admin-secret: <secret>

{
  "keys": [
    {
      "key":              "ap_ojE8S1SzH6i_...",
      "label":            "partner-1",
      "rate_limit_per_hour": 500,
      "is_active":        1,
      "created_at":       1716048000000
    }
  ]
}

Revoke a key

DELETE/admin/keys/:key

Soft-deletes the key by setting is_active = 0. The key stops working immediately on the next request. The record is retained in the database for audit purposes.

bash
DELETE /admin/keys/ap_ojE8S1SzH6i_rrnef7vQ-gmeR-MTd8i8xxxxxxxxxx
x-admin-secret: <secret>

// 200 OK
{ "revoked": true }

Gateway stats

GET/admin/stats

Returns aggregate metrics and the most recent 50 calls.

bash
GET /admin/stats
x-admin-secret: <secret>

{
  "totalCalls":    1042,
  "totalAgents":   3,
  "totalKeys":     7,
  "successCalls":  1038,
  "successRate":   99,           // percentage, 0–100
  "avgDurationMs": 284,
  "recentCalls": [
    {
      "id":            "uuid",
      "agent_id":      "a1b2c3d4",
      "status_code":   200,
      "duration_ms":   284,
      "receipt_hash":  "sha256hex...",
      "created_at":    1716048000000
    }
  ],
  "callsByAgent": [
    { "agent_id": "a1b2c3d4", "total": 1042, "avg_ms": 284 }
  ]
}

Error Reference

HTTPCodeWhen
400Bad RequestMissing required fields, malformed JSON, or invalid receipt encoding.
401UnauthorizedMissing or incorrect x-admin-secret on an admin route.
402Payment RequiredAgent requires payment; no valid x-api-key or x-payment-tx provided.
404Not FoundAgent ID does not exist in the database.
429Too Many RequestsAPI key has exceeded its rateLimitPerHour budget.
500Internal Server ErrorUnexpected gateway error. Check gateway logs.
502Bad GatewayThe upstream Hermes instance returned an error or was unreachable.

All error responses follow the same shape:

bash
{ "error": "Human-readable error message" }

SDK Examples

TypeScript / Node.js

bash
const GATEWAY = 'https://your-gateway.vercel.app'
const API_KEY  = 'ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
const AGENT_ID = 'a1b2c3d4'

async function callAgent(message: string) {
  const res = await fetch(`${GATEWAY}/v1/agents/${AGENT_ID}/run`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      messages: [{ role: 'user', content: message }],
    }),
  })

  if (!res.ok) {
    if (res.status === 429) throw new Error('Rate limit exceeded')
    if (res.status === 402) throw new Error('Payment required')
    throw new Error(`Agent error: ${res.status}`)
  }

  const receipt = res.headers.get('x-agentport-receipt')
  const durationMs = res.headers.get('x-agentport-duration-ms')
  const remaining  = res.headers.get('x-ratelimit-remaining')

  const body = await res.json()
  return { body, receipt, durationMs, remaining }
}

// Verify a receipt locally (no gateway trust required)
import { createHash } from 'crypto'

function verifyReceipt(encoded: string): boolean {
  const r = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'))
  const hash = createHash('sha256')
    .update(`${r.callId}:${r.agentId}:${r.timestamp}:${r.requestHash}:${r.responseHash}`)
    .digest('hex')
  return hash === r.receiptHash
}

Python

bash
import requests, hashlib, base64, json

GATEWAY  = "https://your-gateway.vercel.app"
API_KEY  = "ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
AGENT_ID = "a1b2c3d4"

def call_agent(message: str) -> dict:
    res = requests.post(
        f"{GATEWAY}/v1/agents/{AGENT_ID}/run",
        headers={
            "Content-Type": "application/json",
            "x-api-key": API_KEY,
        },
        json={"messages": [{"role": "user", "content": message}]},
    )

    if res.status_code == 429:
        raise RuntimeError("Rate limit exceeded")
    if res.status_code == 402:
        raise RuntimeError(f"Payment required: {res.json()}")
    res.raise_for_status()

    receipt     = res.headers.get("x-agentport-receipt")
    duration_ms = res.headers.get("x-agentport-duration-ms")
    remaining   = res.headers.get("x-ratelimit-remaining")

    return {
        "body":        res.json(),
        "receipt":     receipt,
        "duration_ms": duration_ms,
        "remaining":   remaining,
    }

def verify_receipt(encoded: str) -> bool:
    r = json.loads(base64.urlsafe_b64decode(encoded + "=="))
    preimage = f"{r['callId']}:{r['agentId']}:{r['timestamp']}:{r['requestHash']}:{r['responseHash']}"
    expected = hashlib.sha256(preimage.encode()).hexdigest()
    return expected == r["receiptHash"]

# Usage
result = call_agent("What is the price of ETH?")
print(result["body"])
print("Receipt valid:", verify_receipt(result["receipt"]))

cURL one-liner

bash
# Full round-trip: call + verify receipt
RECEIPT=$(curl -si \
  -X POST https://your-gateway.vercel.app/v1/agents/a1b2c3d4/run \
  -H "x-api-key: ap_xxx" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Hello"}]}' \
  | grep -i "x-agentport-receipt:" | awk '{print $2}' | tr -d '\r')

# Verify the receipt
curl -X POST https://your-gateway.vercel.app/v1/receipts/verify \
  -H "Content-Type: application/json" \
  -d "{\"receipt\":\"$RECEIPT\"}" | python3 -m json.tool

Ready to deploy?

Clone the repo, set your env vars, deploy the gateway. First agent call in under five minutes.