Deploy to Vercel
Deploy pay as a Vercel container image with an env-backed signer, upstream OpenAPI URL, and stable 402 challenge secret.
Vercel can run OCI container images as Vercel Functions. Use this path when you want a low-ops deployment and your paid API fits Vercel Function limits.
Agent summary
- Put
Dockerfile.vercelat the Vercel project root. - Use
CMD ["server", "start", ...]; no shell entrypoint is needed with currentpayimages. - Pass an upstream OpenAPI or Discovery URL to
--openapiwhen the upstream publishes one. - Use
operator.signer.backend: envand storePAY_OPERATOR_KEYPAIRin Vercel environment variables. - Use the published
latestpayimage. The container image is not version-tagged yet. - Connect the project to Git with Vercel's Container preset so pushes create deployments.
- Set one stable
PAY_MPP_CHALLENGE_SECRETper environment. Paid retries can fail with another402if this value changes across cold starts. - Use the production alias for agent probes unless your preview deployment is publicly accessible.
What you are deploying
Use this layout for a single-service Vercel gateway:
deploy/
provider.yml
Dockerfile.vercelIf the upstream OpenAPI document is not available over https://, add a local openapi.json and copy it into the image.
Provider spec
Use env templates for deploy-time values. operator.signer.backend: env reads PAY_OPERATOR_KEYPAIR at startup and keeps the signer in process memory.
# yaml-language-server: $schema=https://pay.sh/docs-assets/provider.schema.json
name: quotes
subdomain: quotes
title: 'Quote Gateway'
description: 'Paid stock quote endpoint.'
category: finance
version: v1
routing:
type: proxy
url: '${UPSTREAM_BASE_URL}'
auth:
method: header
key: Authorization
value_from_env: UPSTREAM_API_KEY
operator:
currencies:
usd: ['USDC']
network: mainnet
fee_payer: true
rpc_url: '${PAY_RPC_URL}'
recipient: '${PAY_PAYMENT_RECIPIENT}'
challenge_binding_secret: '${PAY_MPP_CHALLENGE_SECRET}'
signer:
backend: env
value_from_env: PAY_OPERATOR_KEYPAIR
endpoints:
- method: GET
path: 'v1/quote/{symbol}'
description: 'Return a quote for one ticker symbol.'
metering:
dimensions:
- direction: usage
unit: requests
scale: 1
tiers:
- price_usd: 0.01PAY_OPERATOR_KEYPAIR must contain a Solana CLI JSON keypair array or a base58-encoded 64-byte keypair. Do not commit the keypair file.
value_from_env takes the environment variable name. Use ${...} templates only for string values that pay expands at startup, such as routing.url, operator.rpc_url, operator.recipient, and operator.challenge_binding_secret.
Container image
ARG PAY_IMAGE=ghcr.io/solana-foundation/pay:latest
FROM ${PAY_IMAGE}
WORKDIR /app
COPY provider.yml /app/provider.yml
CMD ["server", "start", "/app/provider.yml", "--openapi", "https://api.example.com/openapi.json"]This guide relies on the current latest image. Older manually built or cached images may not expand provider-spec env templates or bind to Vercel's PORT environment variable.
When the upstream OpenAPI document is local, copy it into the image:
ARG PAY_IMAGE=ghcr.io/solana-foundation/pay:latest
FROM ${PAY_IMAGE}
WORKDIR /app
COPY provider.yml /app/provider.yml
COPY openapi.json /app/openapi.json
CMD ["server", "start", "/app/provider.yml", "--openapi", "/app/openapi.json"]Multi-service Vercel projects
If the gateway is one service in a larger Vercel project, route traffic with services:
{
"services": {
"gateway": {
"root": ".",
"entrypoint": "Dockerfile.vercel"
}
},
"rewrites": [{ "source": "/(.*)", "destination": { "service": "gateway" } }]
}Environment variables
Set the runtime values as Vercel environment variables:
cd deploy
vercel link
vercel env add UPSTREAM_BASE_URL production
vercel env add UPSTREAM_API_KEY production
vercel env add PAY_RPC_URL production
vercel env add PAY_PAYMENT_RECIPIENT production
vercel env add PAY_MPP_CHALLENGE_SECRET production
vercel env add PAY_OPERATOR_KEYPAIR productionGenerate PAY_MPP_CHALLENGE_SECRET once per environment:
openssl rand -hex 32Keep this value stable across redeploys and cold starts. If the value changes between the initial 402 and the paid retry, the gateway rejects the replay and the client sees another 402.
Deploy from Git
Use a Git-connected Vercel project for normal deployments:
- Push
deploy/provider.ymlanddeploy/Dockerfile.vercelto your repository. - Import or connect the repository in Vercel.
- Select the Container preset.
- Set the project root directory to
deployfor the single-service layout above. - Add the production environment variables, then deploy.
After the first deployment, Vercel builds the container on every push according to your project branch rules.
For a one-off manual deploy, run:
cd deploy
vercel deploy --prodPreview URLs can be protected by Vercel SSO depending on your team settings. Use the production alias, or disable protection for a machine-readable preview URL before probing it with agents.
Verify
Set the service URL:
SERVICE_URL="https://quotes.example.com"Check process health:
curl -fsS "$SERVICE_URL/__402/health"Check the OpenAPI document:
curl -fsS "$SERVICE_URL/openapi.json" | jq '.openapi // .swagger // .info.title'Call a paid endpoint through pay:
pay curl "$SERVICE_URL/v1/quote/AAPL"Read deployment logs when startup or payment verification fails:
vercel logs "$SERVICE_URL"Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Vercel returns 502 or times out before health checks | The container did not bind to Vercel's expected port | Use the current latest image so the gateway reads $PORT. |
| The gateway returns 402 again after payment | PAY_MPP_CHALLENGE_SECRET changed across instances or restarts | Set one stable secret in Vercel env vars and redeploy. |
/__402/health works but your route returns 404 | The path is missing from endpoints[] or has a method mismatch | Add the exact method and path to provider.yml, then redeploy. |
| The upstream returns 401 after payment | The upstream credential was not injected | Check the routing.auth.value_from_env name and the matching Vercel env var. |
| Preview URL redirects to Vercel SSO | Vercel deployment protection is enabled for preview environments | Use the production alias for probes, or disable protection for that preview. |