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:
Authentication — Admin secrets for operators, API keys for callers. Keys are scoped per-agent and revocable without redeploying.
Rate limiting — Per-key sliding-window rate limiter with in-memory state. Limits exposed via standard response headers.
Cryptographic receipts — Every 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 routing — Three 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
Secret required on all /admin/* routes via the x-admin-secret header. Change this in production.
DB_PATH
./agentport.db
Path to the local SQLite database file. Ignored when TURSO_DB_URL is set.
TURSO_DB_URL
—
libsql:// URL for a remote Turso database. When set, overrides DB_PATH. Required for Vercel (ephemeral FS).
TURSO_AUTH_TOKEN
—
Auth token for the Turso database. Required when TURSO_DB_URL is set.
REGISTRY_URL
—
URL of the central AgentPort registry. When set, newly public agents send a beacon POST on registration.
REGISTRY_SECRET
—
Shared secret for authenticating beacon pushes to the registry.
GATEWAY_PUBLIC_URL
—
Public base URL of this gateway, included in beacon payloads so the registry knows how to reach it.
PAYMENT_WALLET
—
Operator wallet address on Base. Callers send AGNP here for x402 payments.
BASE_RPC_URL
https://mainnet.base.org
Base chain RPC endpoint used to verify x402 payment transactions.
NODE_ENV
development
Set to production in production deployments. Affects log level and cookie security.
These apply to the website (apps/website/.env.local).
Variable
Default
Description
GATEWAY_URL
http://localhost:4000
URL the website server-side API routes use to reach the gateway. Should point to your deployed gateway in production.
ADMIN_SECRET
dev-secret
Must match the gateway's ADMIN_SECRET. Used server-side only (not exposed to the browser).
DASHBOARD_PASSWORD
changeme
Password to access the dashboard. Hashed into a session token via HMAC-SHA256.
REGISTRY_SECRET
change-me
Used by the website's /api/registry endpoint to authenticate incoming beacon pushes from gateways.
NEXT_PUBLIC_SITE_URL
http://localhost:3002
Canonical 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.
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
}
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
}
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
Field
Type
Required
Description
x-api-key
string
conditional
Required for agents with pricingMode ≠ free. Format: ap_...
x-payment-tx
string
conditional
x402: on-chain transaction hash of a AGNP transfer to the operator wallet.
Content-Type
string
yes
Must 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
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.
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.