Open Dashboard

Getting Started

Introduction

PayGate turns any HTTP API into an onchain-monetized endpoint using the x402 standard — the HTTP 402 Payment Required status code, finally used as intended. You paste a URL, set a price in MON, and get a proxy URL. Unpaid requests are rejected with structured payment requirements; paid requests are verified against the Monad Testnet and forwarded to your origin in under a second.

There is no SDK. The entire integration surface is two HTTP headers and one smart contract — the PayGateRouter at 0x8197f76729b17C, which settles payments, escrows metered deposits, and manages agent allowances. The protocol takes a 2% fee (200 bps); developers withdraw the rest at any time.

Interactive · x402 Handshake Simulator

AWAITING REQUEST
ClientPayGate ProxyMonad · 10143

$ press simulate to run the packet lifecycle…

Getting Started

Quick Start (60s)

Step 1 — hit a proxy without paying. Every PayGate proxy lives under /api/v1/gate/[proxyId]:

curl -i http://localhost:3000/api/v1/gate/[proxyId]

Step 2 — read the 402. The response is a machine-readable price sheet (this is a live capture from the gateway, not pseudo-JSON):

{
  "x402Version": 2,
  "error": "payment_required",
  "accepts": [{
    "scheme": "exact-native",
    "network": "eip155:10143",
    "asset": "MON",
    "amount": "10000000000000000",
    "payTo": "0x8197f76762F5b2cfeCbdfc1B90FBBAC3FC29b17C",
    "maxTimeoutSeconds": 300,
    "extra": {
      "developer": "0x08e7c5ea6c00047e5fbb9994c7e0409e28c7d1c9",
      "contract": "0x8197f76762F5b2cfeCbdfc1B90FBBAC3FC29b17C",
      "function": "processPayment(address)",
      "chainId": 10143,
      "rpcUrl": "https://testnet-rpc.monad.xyz"
    }
  }],
  "resource": {
    "url": "http://localhost:3000/api/v1/gate/[proxyId]",
    "description": "Instant Translation API"
  }
}

Step 3 — pay onchain and retry. Call processPayment(developer) on the router with amount as msg.value, then resubmit with the receipt in the Payment-Signature header — base64-encoded JSON containing the transaction hash:

# Payment-Signature = base64({"txHash": "0x…", "payer": "0x…"})
SIG=$(echo -n '{"txHash":"0xYOUR_TX_HASH","payer":"0xYOUR_ADDRESS"}' | base64 -w0)

curl -H "Payment-Signature: $SIG" \
  http://localhost:3000/api/v1/gate/[proxyId]

The gateway verifies the transaction onchain (correct contract, correct developer, sufficient value, not replayed) and forwards your request. That is the whole integration.

API Providers

Creating a Proxy

The fastest path is the dashboard: connect a wallet, click New Gateway, paste your origin URL, set a price, and copy the proxy link. Everything the dashboard does is also plain HTTP:

curl -X POST http://localhost:3000/api/endpoints \
  -H "Content-Type: application/json" \
  -d '{
    "walletAddress": "0xYOUR_WALLET",
    "name": "Weather API",
    "targetApiUrl": "https://api.weather.com/v1/current",
    "priceMon": "0.01",
    "billingType": "FLAT"
  }'

Your origin URL is never revealed to consumers — they only ever see the proxy. Earnings accrue inside the router contract under your wallet; call withdrawEarnings() (or click Withdraw in the Treasury tab) to pull the balance at any time.

API Providers

Setting Pricing Models

PayGate supports two billing types per endpoint:

ModelConfigScheme in 402Settlement
FLATpriceMon — fixed MON per requestexact-nativeprocessPayment() before each call; whole price settles instantly
METEREDpricePerByteMon — MON per response bytemetered-escrowdepositEscrow() max cap up front; exact byte cost settles after the response, remainder auto-refunds

Metered deposits are capped at pricePerByteWei × 20,000 bytes. Responses larger than the cap settle at the cap — the consumer can never be charged more than the deposit they escrowed.

curl -X POST http://localhost:3000/api/endpoints \
  -H "Content-Type: application/json" \
  -d '{
    "walletAddress": "0xYOUR_WALLET",
    "name": "LLM Oracle",
    "targetApiUrl": "https://your-model-server.com/generate",
    "billingType": "METERED",
    "pricePerByteMon": "0.000001"
  }'

API Providers

Developer API Keys

Your pg_… key is a Web2 secret bearer token for server-to-server automation. Pass it as Authorization: Bearer pg_… to manage gateways without a browser wallet popup.

It is issued from the dashboard API Keys tab and is completely decoupled from consumer x402 onchain flows. API consumers never see it — they settle MON via the 402 handshake. Regenerating the key immediately invalidates the previous one.

CapabilityAuthNotes
GET /api/endpointsBearer pg_…List all gateways for the authenticated developer
POST /api/endpointsBearer pg_…Create a flat or metered proxy without walletAddress in the body
PATCH / DELETE /api/endpoints/[id]wallet body (dashboard)Mutations still accept wallet ownership; Bearer support coming for CI
Consumer gate callsPayment-Signature / X-Agent-*Never use the developer API key on /api/v1/gate/[proxyId]
# List your gateways programmatically
curl -s http://localhost:3000/api/endpoints \
  -H "Authorization: Bearer pg_YOUR_KEY"

# Register a new flat proxy (no walletAddress required with Bearer)
curl -s -X POST http://localhost:3000/api/endpoints \
  -H "Authorization: Bearer pg_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weather API",
    "targetApiUrl": "https://api.weather.com/v1/current",
    "priceMon": "0.01",
    "billingType": "FLAT"
  }'

Interactive · Developer API Key Tester

AWAITING TOKEN

$ awaiting authentication token…

Consumers / Agents

Handling 402 Errors

A 402 is not a failure — it is the protocol handing you a price sheet. Parse the body and inspect accepts[0]:

FieldMeaning
schemeexact-native (flat) or metered-escrow (per byte)
amountWei to pay (flat price, or max deposit when metered)
payToThe PayGateRouter contract address
extra.developerAddress to pass to processPayment / depositEscrow
extra.functionExact contract function to call
extra.requestIdmetered only — bytes32 key for your escrow
extra.rpcUrl / chainIdWhere to send the transaction

A rejected payment returns 402 again with a reason field — tx_not_found, insufficient_payment_amount, payment_already_used (replay protection), session_allowance_exhausted, and friends — so agents can self-correct without human eyes.

Consumers / Agents

Submitting Receipts

Receipts travel in the Payment-Signature request header as base64-encoded JSON. The shape depends on the scheme:

{
  "txHash": "0x…",   // your processPayment transaction
  "payer": "0x…"     // optional, echoed into analytics
}

Each transaction hash is single-use — the gateway stores it with a unique constraint, so replaying an old receipt yields 402 payment_already_used. On success the response carries X-Payment-Response (base64 JSON settlement summary) and X-PayGate-Settlement (the settlement transaction hash you can verify on the explorer).

Contract Spec

Router Addresses

ParameterValue
ContractPayGateRouter (Solidity 0.8.x, Foundry)
Address0x8197f76762F5b2cfeCbdfc1B90FBBAC3FC29b17C
NetworkMonad Testnet · Chain ID 10143
RPChttps://testnet-rpc.monad.xyz
Explorerhttps://testnet.monadvision.com/address/0x8197f76762F5b2cfeCbdfc1B90FBBAC3FC29b17C
Protocol fee200 bps (2%) on every settlement path
OwnerPayGate platform wallet — sole settlement authority

The owner is the only address permitted to call settleEscrow, refundEscrow, and chargeAgent — enforced by an onlyOwner modifier and verified by fuzzed access-control tests. Consumers and developers never need to trust the proxy with custody: funds sit in the contract, and every state change is checks-effects-interactions ordered.

Contract Spec

Contract Methods

MethodAccessPurpose
processPayment(address developer) payableanyoneFlat payment: 2% fee to treasury, 98% credited to developer balance
withdrawEarnings()developerPull your accrued balance to your wallet
approveAgent(address agent, uint256 allowance) payablemaster walletEscrow msg.value == allowance as a spending cap for an ephemeral agent key
revokeAgent(address agent)master walletKill the session; unspent allowance returns to the master instantly
chargeAgent(master, agent, developer, amount)owner onlySettle one agent request against the escrowed allowance
depositEscrow(address developer, bytes32 requestId) payableanyoneLock a metered max-cap deposit under a unique request id
settleEscrow(bytes32 requestId, uint256 actualCost)owner onlyRelease exact metered cost to the developer, refund the remainder to the consumer
refundEscrow(bytes32 requestId)owner onlyReturn 100% of a locked deposit after an upstream failure
balances / agentAllowances / escrowsviewPublic read access to developer earnings, session caps, escrow records

Core v2

Delegated Session Allowances

(The Corporate Card Pattern)

Agents cannot click through wallet popups. Delegated session allowances fix this by issuing a pre-authorized corporate expense limit to an autonomous agent: the master wallet escrows a spending cap once, and the agent draws against it. You generate a throwaway keypair, escrow an allowance onchain for it, and register it with the gateway. From then on the agent signs plain HTTP headers — zero popups, zero 402s — and the platform settles each request against the escrowed allowance via chargeAgent.

Step 1 — issue the corporate allowance onchain:

import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { parseEther } from "viem";

const agentKey = generatePrivateKey();          // ephemeral, never leaves you
const agent = privateKeyToAccount(agentKey);

// escrow 0.05 MON as the agent's spending cap
await wallet.writeContract({
  address: "0x8197f76762F5b2cfeCbdfc1B90FBBAC3FC29b17C",
  abi: paygateRouterAbi,
  functionName: "approveAgent",
  args: [agent.address, parseEther("0.05")],
  value: parseEther("0.05"),   // msg.value must equal the allowance
});

Step 2 — register with the gateway (it verifies the onchain allowance before accepting):

curl -X POST http://localhost:3000/api/session-keys \
  -H "Content-Type: application/json" \
  -d '{
    "masterAddress": "0xYOUR_WALLET",
    "agentAddress": "0xAGENT_ADDRESS",
    "maxAllowanceWei": "50000000000000000",
    "expiresInHours": 24
  }'

Step 3 — the agent signs requests. Each call carries three headers; the signature is EIP-191 over paygate:agent:<proxyId>:<timestamp> (timestamps older than 120s are rejected):

const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = await agent.signMessage({
  message: `paygate:agent:${proxyId}:${timestamp}`,
});

const res = await fetch(proxyUrl, {
  headers: {
    "X-Agent-Address": agent.address,
    "X-Agent-Signature": signature,
    "X-Agent-Timestamp": timestamp,
  },
});
// -> 200 directly. No 402. No wallet popup. Fire thousands of these.

Revoke any time with revokeAgent(agentAddress) — the unspent allowance returns to the master wallet in the same transaction.

Core v2

Dynamic Payload Metering

(The Taxi Meter Pattern)

Flat pricing overcharges small responses and undercharges large ones. Metered endpoints bill for the exact bytes returned, using an escrow lifecycle the consumer never has to trust:

PhaseCallWhoEffect
1 · LockdepositEscrow(developer, requestId)consumerMax-cap deposit (pricePerByte × 20,000) locked under the 402's requestId
2 · Meter— gateway proxies upstream —PayGateResponse streamed, exact byte length counted: cost = bytes × pricePerByteWei
3 · SettlesettleEscrow(requestId, actualCost)PayGate (owner)Developer gets 98% of actual cost, 2% fee, unspent deposit refunds to the consumer in the same transaction

The settlement summary rides back on the response in the X-Payment-Response header:

{
  "success": true,
  "scheme": "metered-escrow",
  "requestId": "0x50439185b40688b7405618887b605dc235cbf821…",
  "txHash": "0x…",            // the settleEscrow transaction
  "network": "eip155:10143",
  "responseBytes": 252,
  "actualCostWei": "252000000000000",
  "refundedWei": "19748000000000000"
}

Zero-byte responses (e.g. HTTP 204) settle at cost 0 — the full deposit refunds with no protocol fee taken.

Core v2

Deterministic SLA Escrows

(The Vending Machine Pattern)

Under the Vending Machine Pattern, a jammed machine must return the coin. Because metered payments are escrowed rather than paid directly, the contract can enforce this: if the upstream returns a 5xx or times out, the gateway automatically calls refundEscrow(requestId) and 100% of the locked MON returns to the consumer's wallet — no support ticket, no goodwill, just the state machine.

curl -i -H "Payment-Signature: $SIG" \
  http://localhost:3000/api/v1/gate/[proxyId]

# HTTP/1.1 502 Bad Gateway
# X-PayGate-Refund: 0xREFUND_TX_HASH   <- your money is already back
#
# {"error":"upstream_failed","refund":{"txHash":"0x…","amountWei":"20000000000000000"}}

Contract security guarantees

GuaranteeMechanism
No reentrancy on refundsEscrow records are deactivated before any transfer (checks-effects-interactions), proven by reentrancy-probe tests
Only the proxy settlessettleEscrow / refundEscrow / chargeAgent revert with NotOwner for every other caller (fuzz-tested)
No double-settlementAn escrow can settle or refund exactly once; the record deactivates atomically
No replayDeposit tx hashes are unique-constrained; agent signatures expire after 120 seconds
Bounded chargesettleEscrow caps actualCost at the deposit; consumers can never pay more than they locked

The full Foundry suite (33 tests, including fuzzed access control and reentrancy probes) lives in contracts/test/PayGateRouter.t.sol.

402 Payment Required — as intended since 1997