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 0x8197f767…29b17C, 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$ 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:
| Model | Config | Scheme in 402 | Settlement |
|---|---|---|---|
| FLAT | priceMon — fixed MON per request | exact-native | processPayment() before each call; whole price settles instantly |
| METERED | pricePerByteMon — MON per response byte | metered-escrow | depositEscrow() 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.
| Capability | Auth | Notes |
|---|---|---|
| GET /api/endpoints | Bearer pg_… | List all gateways for the authenticated developer |
| POST /api/endpoints | Bearer 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 calls | Payment-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]:
| Field | Meaning |
|---|---|
| scheme | exact-native (flat) or metered-escrow (per byte) |
| amount | Wei to pay (flat price, or max deposit when metered) |
| payTo | The PayGateRouter contract address |
| extra.developer | Address to pass to processPayment / depositEscrow |
| extra.function | Exact contract function to call |
| extra.requestId | metered only — bytes32 key for your escrow |
| extra.rpcUrl / chainId | Where 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
| Parameter | Value |
|---|---|
| Contract | PayGateRouter (Solidity 0.8.x, Foundry) |
| Address | 0x8197f76762F5b2cfeCbdfc1B90FBBAC3FC29b17C |
| Network | Monad Testnet · Chain ID 10143 |
| RPC | https://testnet-rpc.monad.xyz |
| Explorer | https://testnet.monadvision.com/address/0x8197f76762F5b2cfeCbdfc1B90FBBAC3FC29b17C |
| Protocol fee | 200 bps (2%) on every settlement path |
| Owner | PayGate 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
| Method | Access | Purpose |
|---|---|---|
| processPayment(address developer) payable | anyone | Flat payment: 2% fee to treasury, 98% credited to developer balance |
| withdrawEarnings() | developer | Pull your accrued balance to your wallet |
| approveAgent(address agent, uint256 allowance) payable | master wallet | Escrow msg.value == allowance as a spending cap for an ephemeral agent key |
| revokeAgent(address agent) | master wallet | Kill the session; unspent allowance returns to the master instantly |
| chargeAgent(master, agent, developer, amount) | owner only | Settle one agent request against the escrowed allowance |
| depositEscrow(address developer, bytes32 requestId) payable | anyone | Lock a metered max-cap deposit under a unique request id |
| settleEscrow(bytes32 requestId, uint256 actualCost) | owner only | Release exact metered cost to the developer, refund the remainder to the consumer |
| refundEscrow(bytes32 requestId) | owner only | Return 100% of a locked deposit after an upstream failure |
| balances / agentAllowances / escrows | view | Public 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:
| Phase | Call | Who | Effect |
|---|---|---|---|
| 1 · Lock | depositEscrow(developer, requestId) | consumer | Max-cap deposit (pricePerByte × 20,000) locked under the 402's requestId |
| 2 · Meter | — gateway proxies upstream — | PayGate | Response streamed, exact byte length counted: cost = bytes × pricePerByteWei |
| 3 · Settle | settleEscrow(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
| Guarantee | Mechanism |
|---|---|
| No reentrancy on refunds | Escrow records are deactivated before any transfer (checks-effects-interactions), proven by reentrancy-probe tests |
| Only the proxy settles | settleEscrow / refundEscrow / chargeAgent revert with NotOwner for every other caller (fuzz-tested) |
| No double-settlement | An escrow can settle or refund exactly once; the record deactivates atomically |
| No replay | Deposit tx hashes are unique-constrained; agent signatures expire after 120 seconds |
| Bounded charge | settleEscrow 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.