API reference
Analytics
Merchant-account sales analytics over the account's payments (as payee). Amounts are token base units.
Merchant sales KPIs (counts, per-token/chain volume, per-chain gas)
GET /analytics/summary
Bearer JWT (Sign-In With Ethereum)
Account-only (require_account!): headline sales analytics over the merchant account's own payments as payee. 403 for an account-less (buyer) session. Scope: every wallet of the account by default; payee narrows it to ONE of the account's wallets (case-insensitive; a deactivated wallet is still readable), which also narrows the gas and failure rows to that wallet's broadcasts. A payee that is not a wallet of the session's account is 403 forbidden; a malformed one is 400. Counts are token-agnostic; monetary volume is grouped per (token, chain) and only summed within one token; gas is grouped per chain and denominated in that chain's native token, so it is never summed across chains either, and is additionally sliced by payment status and by operation.
Query parameters
-
payee string
One of the account's wallet addresses (0x, 40 hex, any case) — scopes the analytics to that wallet only. Omitted: every wallet of the account. 403 when it is not a wallet of the session's account.
-
mode stringOne of:
authorize,charge -
status stringOne of:
unsigned,signed,authorized,charged,captured,partially_captured,expired,voided,released,refunded,partially_refunded -
token string
Token address (0x…) — scopes volume to one token.
-
chain_id integer
-
from string (date-time)
Only payments created at/after this time (ISO-8601).
-
to string (date-time)
Only payments created at/before this time (ISO-8601).
Responses
-
200
Order counts, by-status counts, refund/dispute rates, and per-(token, chain) volume: gross authorized, net settled to the payee, still in escrow, and gross captured/refunded from the confirmed transactions (base units). Plus
gasper chain —spenton confirmed transactions andwastedby on-chain reverts, as wei-scale base-unit strings in the chain's native token (decimals 18) — withconfirmed/failedresolved transaction counts, andfailed_ratederived from them (per resolved transaction, not per order).gas_by_statusandgas_by_operationare those same rows regrouped, each carrying akey(the payment status / the operation) alongside the same fields; every cut sums back to its chain'sgasrow. The status cut is a SNAPSHOT — a payment's status moves and its gas moves with it, so the same period changes over time; the operation cut is stable. Each chain/status row carriesorders, the count of payments behind it (including those that produced no transaction), so(spent + wasted) / ordersis the average cost of an order in that state; it is null on the operation cut, where one order spans several operations andspent / confirmedis the meaningful average instead. Gas covers the operations the merchant broadcasts: dispute/close_dispute are the buyer's cost and are excluded; a release counts when its recordedsenderis one of the account's wallets, and one with an unknown sender (a report-by-hash submit) stays out. Alsofailures— one row per decoded failure code with the number of the merchant's transactions that hit it, commonest first — andconfirmation_secson each gas row: the mean seconds from broadcast to confirmation on that chain, weighted by its confirmations and null when none confirmed. - 401 Authentication failed.
- 403 Caller not permitted.
curl "https://api.rail0.xyz/analytics/summary" \ -H "Authorization: Bearer $RAIL0_TOKEN"const res = await fetch("https://api.rail0.xyz/analytics/summary", { headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/analytics/summary")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/analytics/summary" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "orders": 128, "disputed": 2, "refund_rate": 0.0469, "dispute_rate": 0.0156, "failed_rate": 0.0122, "by_status": { "authorized": 9, "captured": 104, "partially_captured": 4, "refunded": 6, "voided": 3, "expired": 2 }, "failures": [ { "code": "insufficient_token_balance", "transactions": 2 }, { "code": "authorization_expired", "transactions": 1 } ], "volume": [ { "chain_id": 84532, "chain_name": "Base Sepolia", "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "symbol": "USDC", "decimals": 6, "orders": 128, "gross": "6412500000", "settled": "5980250000", "escrowed": "214000000", "captured": "6198500000", "refunded": "218250000" } ], "gas": [ { "chain_id": 84532, "chain_name": "Base Sepolia", "symbol": "ETH", "decimals": 18, "orders": 128, "spent": "1843200000000000", "wasted": "21400000000000", "confirmed": 243, "failed": 3, "confirmation_secs": 4.2 } ], "gas_by_status": [ { "key": "captured", "chain_id": 84532, "chain_name": "Base Sepolia", "symbol": "ETH", "decimals": 18, "orders": 104, "spent": "1532800000000000", "wasted": "14200000000000", "confirmed": 208, "failed": 2, "confirmation_secs": 4.1 } ], "gas_by_operation": [ { "key": "capture", "chain_id": 84532, "chain_name": "Base Sepolia", "symbol": "ETH", "decimals": 18, "orders": null, "spent": "742600000000000", "wasted": "7200000000000", "confirmed": 112, "failed": 1, "confirmation_secs": 4 } ]}Order count per time bucket
GET /analytics/timeseries
Bearer JWT (Sign-In With Ethereum)
Account-only. Order count per time interval (day/week/month), oldest first. Accepts the same payee scope as /analytics/summary (403 forbidden for a payee that is not a wallet of the session's account). Per-bucket volume is included only when both a token and a chain are filtered (so the sum is over one token); otherwise null.
Query parameters
-
payee string
One of the account's wallet addresses (0x, 40 hex, any case) — scopes the analytics to that wallet only. Omitted: every wallet of the account. 403 when it is not a wallet of the session's account.
-
mode stringOne of:
authorize,charge -
status stringOne of:
unsigned,signed,authorized,charged,captured,partially_captured,expired,voided,released,refunded,partially_refunded -
token string
Token address (0x…) — scopes volume to one token.
-
chain_id integer
-
from string (date-time)
Only payments created at/after this time (ISO-8601).
-
to string (date-time)
Only payments created at/before this time (ISO-8601).
-
interval stringOne of:
day,week,month
Responses
- 200 Buckets with order count and (single-token) volume.
- 401 Authentication failed.
- 403 Caller not permitted.
curl "https://api.rail0.xyz/analytics/timeseries?interval=day" \ -H "Authorization: Bearer $RAIL0_TOKEN"const res = await fetch("https://api.rail0.xyz/analytics/timeseries?interval=day", { headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/analytics/timeseries?interval=day")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/analytics/timeseries?interval=day" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}[ { "bucket": "2026-10-01T00:00:00Z", "orders": 18, "volume": null }, { "bucket": "2026-10-02T00:00:00Z", "orders": 24, "volume": null }, { "bucket": "2026-10-03T00:00:00Z", "orders": 21, "volume": null }]Orders aggregated by a dimension
GET /analytics/breakdown
Bearer JWT (Sign-In With Ethereum)
Account-only. Aggregate orders by token, chain, mode, or status. Accepts the same payee scope as /analytics/summary (403 forbidden for a payee that is not a wallet of the session's account). token rows carry per-token volume; chain, mode and status rows are counts only (volume is never summed across tokens).
Query parameters
-
payee string
One of the account's wallet addresses (0x, 40 hex, any case) — scopes the analytics to that wallet only. Omitted: every wallet of the account. 403 when it is not a wallet of the session's account.
-
mode stringOne of:
authorize,charge -
status stringOne of:
unsigned,signed,authorized,charged,captured,partially_captured,expired,voided,released,refunded,partially_refunded -
token string
Token address (0x…) — scopes volume to one token.
-
chain_id integer
-
from string (date-time)
Only payments created at/after this time (ISO-8601).
-
to string (date-time)
Only payments created at/before this time (ISO-8601).
-
by string requiredOne of:
token,chain,mode,status,operation
Responses
- 200 One row per dimension key with an order count (and volume for token).
- 401 Authentication failed.
- 403 Caller not permitted.
curl "https://api.rail0.xyz/analytics/breakdown?by=token" \ -H "Authorization: Bearer $RAIL0_TOKEN"const res = await fetch("https://api.rail0.xyz/analytics/breakdown?by=token", { headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/analytics/breakdown?by=token")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/analytics/breakdown?by=token" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}[ { "key": "USDC", "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "chain_id": 84532, "decimals": 6, "orders": 112, "volume": "5612500000" }, { "key": "EURC", "token": "0x808456652fdb597867f38412077A9182bf77359F", "chain_id": 84532, "decimals": 6, "orders": 16, "volume": "800000000" }]