Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ REDIS_URL=redis://localhost:6379
PRICE_CACHE_TTL=10
# How long to cache GET /snapshot responses per network in seconds (default: 15)
SNAPSHOT_CACHE_TTL=15
# Maximum age in seconds before /premium price data is considered stale and rejected (default: 60)
PREMIUM_STALENESS_SECONDS=60
# Default timeout for outbound HTTP requests in milliseconds (default: 10000)
HTTP_TIMEOUT_MS=10000

Expand Down
168 changes: 168 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,89 @@
}
}
},
"/premium/{assetA}/{assetB}": {
"get": {
"operationId": "getPremium",
"summary": "Get on-chain vs CEX price premium and spread",
"description": "Returns the on-chain price, the best CEX price, the spread in basis points, both sources, and both timestamps. Refuses to answer with a 400 when either side is older than the configured staleness bound.",
"security": [],
"parameters": [
{
"name": "assetA",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"example": "XLM"
},
{
"name": "assetB",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"example": "USDC"
},
{
"$ref": "#/components/parameters/NetworkQuery"
}
],
"responses": {
"200": {
"description": "Premium and spread calculation",
"headers": {
"Cache-Control": {
"$ref": "#/components/headers/CacheControl"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PremiumResponse"
}
}
}
},
"400": {
"description": "Stale price data or unrecognised network",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PremiumErrorResponse"
}
}
}
},
"429": {
"$ref": "#/components/responses/TooManyRequests"
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"error",
"code"
],
"properties": {
"error": {
"type": "string"
},
"code": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/price/{assetA}/{assetB}/route": {
"get": {
"operationId": "getBestRoute",
Expand Down Expand Up @@ -4059,6 +4142,91 @@
}
}
}
},
"PremiumPricePoint": {
"type": "object",
"required": [
"price",
"source",
"timestamp"
],
"properties": {
"price": {
"type": "number"
},
"source": {
"type": "string"
},
"timestamp": {
"type": "string",
"format": "date-time"
}
}
},
"PremiumResponse": {
"type": "object",
"required": [
"assetA",
"assetB",
"network",
"onChain",
"cex",
"spreadBps"
],
"properties": {
"assetA": {
"type": "string"
},
"assetB": {
"type": "string"
},
"network": {
"type": "string"
},
"onChain": {
"$ref": "#/components/schemas/PremiumPricePoint"
},
"cex": {
"$ref": "#/components/schemas/PremiumPricePoint"
},
"spreadBps": {
"type": "number"
}
}
},
"PremiumErrorResponse": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string"
},
"code": {
"type": "string"
},
"stale": {
"type": "string",
"enum": [
"onChain",
"cex",
"both"
]
},
"ageSeconds": {
"type": "integer",
"nullable": true
},
"onChainAgeSeconds": {
"type": "integer",
"nullable": true
},
"cexAgeSeconds": {
"type": "integer",
"nullable": true
}
}
}
}
}
Expand Down
104 changes: 104 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,60 @@ paths:
'429':
$ref: '#/components/responses/TooManyRequests'

/premium/{assetA}/{assetB}:
get:
operationId: getPremium
summary: Get on-chain vs CEX price premium and spread
description: >-
Returns the on-chain price, the best CEX price, the spread in basis
points, both sources, and both timestamps. Refuses to answer with a 400
when either side is older than the configured staleness bound.
security: []
parameters:
- name: assetA
in: path
required: true
schema:
type: string
example: XLM
- name: assetB
in: path
required: true
schema:
type: string
example: USDC
- $ref: '#/components/parameters/NetworkQuery'
responses:
'200':
description: Premium and spread calculation
headers:
Cache-Control:
$ref: '#/components/headers/CacheControl'
content:
application/json:
schema:
$ref: '#/components/schemas/PremiumResponse'
'400':
description: Stale price data or unrecognised network
content:
application/json:
schema:
$ref: '#/components/schemas/PremiumErrorResponse'
'500':
description: Internal server error
content:
application/json:
schema:
type: object
required: [error, code]
properties:
error:
type: string
code:
type: string
'429':
$ref: '#/components/responses/TooManyRequests'

/price/{assetA}/{assetB}/route:
get:
operationId: getBestRoute
Expand Down Expand Up @@ -2596,3 +2650,53 @@ components:
properties:
message:
type: string

PremiumPricePoint:
type: object
required: [price, source, timestamp]
properties:
price:
type: number
source:
type: string
timestamp:
type: string
format: date-time

PremiumResponse:
type: object
required: [assetA, assetB, network, onChain, cex, spreadBps]
properties:
assetA:
type: string
assetB:
type: string
network:
type: string
onChain:
$ref: '#/components/schemas/PremiumPricePoint'
cex:
$ref: '#/components/schemas/PremiumPricePoint'
spreadBps:
type: number

PremiumErrorResponse:
type: object
required: [error]
properties:
error:
type: string
code:
type: string
stale:
type: string
enum: [onChain, cex, both]
ageSeconds:
type: integer
nullable: true
onChainAgeSeconds:
type: integer
nullable: true
cexAgeSeconds:
type: integer
nullable: true
Loading