Testing
Set up devnet/testnet environments for local development and testing.
PincerPay supports devnet and testnet chains for development. Use these to test the full payment flow without spending real USDC.
Zero-setup option: The interactive demo simulates the full payment flow in your browser with no wallet, tokens, or environment setup required.
Devnet Configuration
Merchant
Point your paywall at a devnet chain:
pincerpay({
apiKey: process.env.PINCERPAY_API_KEY!,
merchantAddress: "YOUR_DEVNET_WALLET",
routes: {
"GET /api/weather": {
price: "0.01",
chain: "solana-devnet",
description: "Weather data",
},
},
})
Agent
Match the merchant's chain:
const agent = await PincerPayAgent.create({
chains: ["solana-devnet"],
solanaPrivateKey: process.env.AGENT_SOLANA_KEY!,
});
Getting Test USDC
| Chain | Faucet |
|---|---|
| Solana Devnet | Circle faucet — select Solana, USDC |
| Base Sepolia | Coinbase faucet |
| Polygon Amoy | Polygon faucet |
Verifying Transactions
After running a test payment:
- Dashboard — check the Transactions page for status (
pending→confirmed) - Solana Explorer — paste the tx hash at explorer.solana.com/?cluster=devnet
- Base Sepolia Explorer — paste the tx hash at sepolia.basescan.org
End-to-End Test Script
A complete test from agent to merchant:
import express from "express";
import { pincerpay } from "@pincerpay/merchant";
import { PincerPayAgent } from "@pincerpay/agent";
// 1. Start merchant
const app = express();
app.use(
pincerpay({
apiKey: process.env.PINCERPAY_API_KEY!,
merchantAddress: process.env.MERCHANT_WALLET!,
routes: {
"GET /api/data": {
price: "0.001",
chain: "solana-devnet",
description: "Test data",
},
},
})
);
app.get("/api/data", (req, res) => res.json({ result: "success" }));
const server = app.listen(4000);
// 2. Create agent and pay
const agent = await PincerPayAgent.create({
chains: ["solana-devnet"],
solanaPrivateKey: process.env.AGENT_SOLANA_KEY!,
});
const res = await agent.fetch("http://localhost:4000/api/data");
console.log(await res.json()); // { result: "success" }
server.close();
Webhook Testing
Test webhook delivery to confirm your integration receives payment events correctly.
- Set up a webhook URL. For local development, use a tunnel service like ngrok (
ngrok http 4000) or a hosted listener like webhook.site to get a publicly reachable URL. - Configure the webhook URL in the PincerPay dashboard under Settings > Webhooks. Copy the webhook secret displayed after saving.
- Make a test payment using the devnet flow described above. The facilitator fires a
payment.completedevent to your webhook URL on settlement. - Check delivery status by calling
GET /v1/webhooks. Each entry shows the HTTP status code returned by your endpoint and the timestamp of the attempt. - Retry failed webhooks by calling
POST /v1/webhooks/:id/retry. This re-sends the same payload with a fresh signature.
Verifying Webhook Signatures
Every webhook request includes an X-PincerPay-Signature header with the format:
t=<timestamp>,v1=<hmac-hex>
To verify the signature:
- Parse the
t(Unix timestamp) andv1(HMAC hex digest) values from the header. - Reconstruct the signed content by concatenating
<timestamp>.<raw-body>. - Compute an HMAC-SHA256 using the webhook secret from the dashboard as the key.
- Compare the computed digest to
v1using a timing-safe comparison. - Optionally, validate that the timestamp is within an acceptable tolerance (e.g., 5 minutes) to prevent replay attacks.
Node.js
import crypto from "node:crypto";
function verifyWebhookSignature(
payload: string,
header: string,
secret: string,
toleranceSec = 300,
): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string])
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
// Replay protection
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (age > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${payload}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Python
import hmac, hashlib, time
def verify_webhook(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = parts.get("t"), parts.get("v1")
if not ts or not sig:
return False
if time.time() - int(ts) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{ts}.{payload.decode()}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(sig, expected)
Common Error Scenarios
| Error | Cause | Fix |
|---|---|---|
| 402 with no payment | Agent did not include the x-payment header. |
Verify the agent is configured for the same chain as the merchant (e.g., both on solana-devnet). |
| 403 spending limit | Agent exceeded the per-transaction or daily spending limit. | Adjust the agent's spending policies in the dashboard. |
| 500 on settlement | Usually an RPC node error or network timeout. | Check the facilitator health endpoint (GET /health) and review facilitator logs for RPC connection issues. |
Rate Limit Testing
The facilitator enforces per-IP rate limits. Default thresholds:
| Endpoint | Limit |
|---|---|
| Global | 120 req/min |
| Settle | 50 req/min |
| Verify | 100 req/min |
Rate limit state is communicated via response headers:
X-RateLimit-Limit-- the maximum number of requests allowed in the window.X-RateLimit-Remaining-- how many requests remain before throttling.X-RateLimit-Reset-- Unix timestamp when the window resets.
When the limit is exceeded, the facilitator returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.