Paywalls
Generated HTTP reference for the 4 operations the published OpenAPI document describes under paywalls.
Paywalls
This page is generated from the published OpenAPI document. It is complete with respect to that document and says nothing about surfaces the document does not describe yet. See what is generated here for what that means.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
REST API for Gate, the x402 gateway. A paywall charges agents per call for an endpoint you own, settling in USDC over x402.
Base URL: https://api.codespar.dev
All endpoints require authentication. See Authentication.
Endpoints marked admin require a bearer api key OR service auth with an
x-codespar-user header for an account admin/owner. Paywalls are
project-scoped.
Paywall object
| Field | Type | Description |
|---|---|---|
id | string | Paywall ID, pw_<...> |
project_id | string | Owning project |
slug | string | URL slug; global namespace. /^[a-z0-9_-]+$/ |
name | string | Display name, max 128 |
upstream_url | string | Where paid calls are proxied |
price | string | USDC per call, decimal (min "0.01") |
price_atomic | string | Same price in atomic USDC (6 dp) |
currency | string | USDC |
pricing_model | string | flat (default), tiered, dynamic, or metered |
payto_kind | "provisioned" | "byo" | Where settlement lands |
payto_address | string | null | The 0x address for byo |
consumer_id | string | null | The consumer whose wallet receives for provisioned |
active | boolean | Paused paywalls return 402 with no settlement |
gateway_url | string | https://gw.codespar.dev/<slug> |
created_at | string | ISO 8601 |
Create a paywall
POST /v1/paywalls (admin)
Body
{
"slug": "market-data",
"name": "Market data API",
"upstream_url": "https://api.yourservice.com/quote",
"price": "0.01",
"currency": "USDC",
"consumer_id": "your-consumer",
"payto": { "kind": "provisioned" }
}payto.kind: "provisioned"— settles to the CodeSpar wallet ofconsumer_id(required for this kind).payto.kind: "byo"— settles topayto.address(a0xEVM address).
Response — 201 Created with the paywall object.
Pricing models
pricing_model selects how the per-call price is resolved:
| Value | Price per call | Status |
|---|---|---|
flat | price, the same on every call. The default | Live |
tiered | price steps down as the paywall's settled count crosses configured thresholds | Live |
dynamic | Resolved per call from the paywall's dynamic rule | Live |
metered | A ceiling is charged up front, then the actual cost is metered from the upstream response and the difference is refunded on-chain. See Meter | Beta |
Gateway protocol
The 402 challenge is delivered base64-encoded in the PAYMENT-REQUIRED
response header (x402Version 2). The JSON body of the 402 is a
human-readable hint: clients read the header, not the body.
Payment goes back in the PAYMENT-SIGNATURE request header. X-PAYMENT is
still accepted for x402 v1 compatibility, and a v2 client may send both.
The settlement result comes back in the PAYMENT-RESPONSE response header
(also mirrored to X-PAYMENT-RESPONSE for v1 clients). Both are listed in
Access-Control-Expose-Headers, so a browser client can read them.
An optional Idempotency-Key request header makes a paywall call settle at
most once, even across re-signed retries. A retry that arrives while the first
is still in flight gets 409 idempotency_in_progress; a completed call
replays the stored response with Idempotent-Replay: true.
List paywalls
GET /v1/paywalls
Response
{ "paywalls": [/* Paywall */] }Get a paywall
GET /v1/paywalls/:id — the paywall object, or 404 paywall_not_found.
Update a paywall
PATCH /v1/paywalls/:id (admin)
Body (at least one field)
{ "name": "...", "upstream_url": "...", "price": "0.02", "active": false }Set active: false to pause (callers get 402, nothing settles); true to resume.
Delete a paywall
DELETE /v1/paywalls/:id (admin) — 204 No Content. Paywalls are config, not
audit data; settled receipts are retained independently.
Earnings
GET /v1/paywalls/:id/stats
Read-only aggregate over the paywall's settled payments.
Response
{
"paywall_id": "pw_...",
"slug": "market-data",
"currency": "USDC",
"settled_count": 128,
"gross_atomic": "1280000",
"gross": "1.28",
"refunded_atomic": "0",
"refunded": "0.00",
"net_atomic": "1280000",
"net": "1.28",
"last_settled_at": "2026-07-10T21:04:11Z"
}gross is what payers authorized, refunded is what went back to them, and
net is what the paywall kept. On flat, tiered, and dynamic paywalls there are
no refunds, so gross equals net. On a metered
paywall gross is the sum of the signed ceilings and net is the actual
metered revenue, so the two read as ceiling versus actual.
Errors
| HTTP | code | when |
|---|---|---|
400 | invalid_body | body failed validation |
403 | forbidden | non-admin caller on an admin endpoint |
404 | paywall_not_found | unknown id in this project |
409 | slug_conflict | the slug is already taken |
See also
- Monetized MCP servers: per-tool
pricing for an MCP server you already run, managed on
/v1/mcp-serversand served atgw.codespar.dev/mcp/<slug>. - Getting discovered: the public
manifest at
gw.codespar.dev/.well-known/x402that lists your active paywalls with their x402 terms.
Every operation, from the spec
Generated from the published OpenAPI document, so it never drifts from what the API actually serves. The section above is written by hand and carries what a schema cannot: the object model, field rules, and the order to call things in.
GET /v1/paywalls
List paywalls
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
paywalls | array of object | yes | — |
Example response
{
"paywalls": [
{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"name": "Example",
"upstream_url": "https://example.com/hook",
"price": "string",
"price_atomic": "string",
"currency": "BRL",
"environment": "live",
"payto_kind": "byo",
"payto_address": "string",
"pricing_model": "flat",
"metered_config": {
"basis": "string",
"base_atomic": "string",
"min_mult": 1,
"max_mult": 1,
"units_header": "string"
},
"active": true,
"gateway_url": "https://example.com/hook",
"created_at": "2026-01-15T12:00:00.000Z"
}
]
}Example request
curl -X GET https://api.codespar.dev/v1/paywalls \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/paywalls/{id}
Read one paywall
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | Not Found. A paywall owned by another org or another project is indistinguishable from one that does not exist: the lookup filters on id AND org AND project in the same query, so a cross-tenant read is 404 and never 403. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | yes | The gateway resolves ACTIVE rows only; an inactive paywall stops serving. |
category | string,null | yes | — |
consumer_id | string,null | yes | The consumer whose wallet receives, for a provisioned payTo. Null for 'byo'. |
created_at | string (date-time) | yes | — |
currency | string | yes | 'USDC' on every row the create route writes; the column has no CHECK. |
description | string,null | yes | — |
dynamic_price_url | string,null | yes | Seller price hook for 'dynamic': the gateway GETs it per request and quotes what it returns. It is fail-safe, not fail-closed: an unsafe, unreachable, non-2xx, malformed or timed-out hook falls back to price_atomic, so the gateway can always quote a price. |
environment | "live" | "test" | yes | Inherited from the calling credential's project at create time. |
gateway_url | string | yes | https://gw.codespar.dev/\<slug\>, built from the slug on the way out. The id does not appear in it. |
id | string | yes | pw_ followed by a 16-character nanoid. |
metered_config | object,null | yes | Config for 'metered'. Null for every other model. |
methods | array,null | yes | HTTP methods this paywall fronts. Null means any method. A method that is not listed is refused by the gateway with 405 method_not_allowed and an Allow header, BEFORE any 402 challenge, so it cannot be paid for and then proxied. |
name | string | yes | — |
payto_address | string | yes | The 0x address that receives settlement. |
payto_kind | "byo" | "provisioned" | yes | 'byo': the seller supplied the receiving 0x USDC address. 'provisioned': CodeSpar derived and owns a CDP address for the tenant's consumer. |
price | string | yes | price_atomic rendered as a decimal USDC string with trailing zeros trimmed ('1000' becomes '0.001'). Derived on the way out, not stored. |
price_atomic | string | yes | The stored price: USDC atomic units (6 decimals) as an integer string, to keep the x402 UintString convention exact. |
pricing_model | "flat" | "token" | "dynamic" | "time" | "per_unit" | "tiered" | "metered" | yes | The full set a row may hold. Only 'flat', 'tiered', 'dynamic' and 'metered' can be created today; the create route refuses the other three with 400 pricing_model_unsupported rather than storing a model that would bill as flat. |
pricing_tiers | array,null | yes | Ascending price curve for 'tiered'. Null for every other model. |
project_id | string | yes | — |
slug | string | yes | The public gateway key. UNIQUE GLOBALLY, not per project: the uniqueness index covers the slug alone, so a slug is first-come across all tenants and a taken one is refused with 400 slug_conflict on create. |
upstream_url | string | yes | Origin the gateway proxies to once a call is paid for. |
Example response
{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"name": "Example",
"upstream_url": "https://example.com/hook",
"price": "string",
"price_atomic": "string",
"currency": "BRL",
"environment": "live",
"payto_kind": "byo",
"payto_address": "string",
"pricing_model": "flat",
"metered_config": {
"basis": "string",
"base_atomic": "string",
"min_mult": 1,
"max_mult": 1,
"units_header": "string"
},
"active": true,
"gateway_url": "https://example.com/hook",
"created_at": "2026-01-15T12:00:00.000Z"
}Example request
curl -X GET https://api.codespar.dev/v1/paywalls/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"DELETE /v1/paywalls/{id}
Delete a paywall
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
204 | — | No Content |
403 | object | Forbidden. The acting user does not satisfy the org-role guard in front of this route. With an OAuth access token this is the live behaviour when x-codespar-user is absent or names a user below admin; with an API key it appears only where the deployment enforces the same guard. |
404 | object | Not Found. A paywall owned by another org or another project is indistinguishable from one that does not exist: the lookup filters on id AND org AND project in the same query, so a cross-tenant read is 404 and never 403. |
Example request
curl -X DELETE https://api.codespar.dev/v1/paywalls/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/paywalls/{id}/stats
Earnings for one paywall
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | Not Found. A paywall owned by another org or another project is indistinguishable from one that does not exist: the lookup filters on id AND org AND project in the same query, so a cross-tenant read is 404 and never 403. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | yes | — |
gross | string | yes | gross_atomic as a decimal USDC string. |
gross_atomic | string | yes | Sum of what payers authorized, in USDC atomic units. |
last_settled_at | string,null (date-time) | yes | Most recent settlement, UTC. Null when nothing has settled. |
net | string | yes | net_atomic as a decimal USDC string. |
net_atomic | string | yes | gross_atomic minus refunded_atomic, clamped at zero rather than allowed to go negative. Flat, tiered and dynamic paywalls have no refunds, so net equals gross there; on the metered lane the payer signs a ceiling and the difference goes back on-chain, so gross alone would report money the seller does not keep. |
paywall_id | string | yes | — |
refunded | string | yes | refunded_atomic as a decimal USDC string. |
refunded_atomic | string | yes | Sum of the metered refunds that are owed or already gone: attempts in status 'claimed', 'sent' or 'confirmed'. The refund lane writes five statuses and only these three are subtracted; the other two, 'waived' (below the dust floor, so no transfer is made) and 'failed' (the transfer or its transaction reverted), leave the money with the seller. |
settled_count | integer | yes | Settled gateway calls attributed to this paywall. |
slug | string | yes | — |
Example response
{
"paywall_id": "paywall_0000000000000000",
"slug": "example",
"currency": "BRL",
"settled_count": 1,
"gross_atomic": "string",
"gross": "string",
"refunded_atomic": "string",
"refunded": "string",
"net_atomic": "string",
"net": "string"
}Example request
curl -X GET https://api.codespar.dev/v1/paywalls/{id}/stats \
-H "Authorization: Bearer $CODESPAR_API_KEY"