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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,13 @@ LP_POOL_CONTRACT_IDS_MAINNET=
# HTTP tests run against a seeded database without an ingest racing them, and
# without needing live RPC. Not for production.
SKIP_INDEXER=false

# ─── Platform fee ledger (GET /fees/report) ──────────────────────────────────
# Every fee GET /fees quotes is recorded with the policy that produced it.
# A quote whose fee exceeds this percent of the amount it priced raises an
# alarm: the row is flagged, `veil_fee_quote_alarms_total` increments, and a
# line is logged. Default 10 — the rate the fee policy itself calls stark.
VEIL_FEE_ALARM_PERCENT=10
# Bearer token for GET /fees/report. Unset means the report answers 503, so it
# is never served open by accident. Use a long random value.
VEIL_FEE_REPORT_TOKEN=
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ services:
LP_POOL_CONTRACT_IDS_TESTNET: ${LP_POOL_CONTRACT_IDS_TESTNET:-}
LP_POOL_CONTRACT_IDS_MAINNET: ${LP_POOL_CONTRACT_IDS_MAINNET:-}
SKIP_INDEXER: ${SKIP_INDEXER:-false}
VEIL_FEE_ALARM_PERCENT: ${VEIL_FEE_ALARM_PERCENT:-10}
VEIL_FEE_REPORT_TOKEN: ${VEIL_FEE_REPORT_TOKEN:-}
ports:
- "${PORT:-3000}:3000"

Expand Down
277 changes: 277 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
}
],
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
},
"schemas": {},
"parameters": {}
},
Expand Down Expand Up @@ -5990,6 +5996,277 @@
}
}
}
},
"/fees/report": {
"get": {
"summary": "Platform fees charged, per UTC day and asset, with the policy behind them",
"description": "Internal endpoint. Not part of the public data API: an operator report over the fee ledger, behind the VEIL_FEE_REPORT_TOKEN bearer token. Nothing in it identifies a person.",
"x-internal": true,
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"schema": {
"type": "string",
"nullable": true,
"description": "First UTC date to report, YYYY-MM-DD. Defaults to today."
},
"required": false,
"description": "First UTC date to report, YYYY-MM-DD. Defaults to today.",
"name": "from",
"in": "query"
},
{
"schema": {
"type": "string",
"nullable": true,
"description": "Last UTC date to report, inclusive, YYYY-MM-DD. Defaults to `from`. At most 92 days after it."
},
"required": false,
"description": "Last UTC date to report, inclusive, YYYY-MM-DD. Defaults to `from`. At most 92 days after it.",
"name": "to",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"network": {
"type": "string"
},
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"alarmPercent": {
"type": "number",
"description": "VEIL_FEE_ALARM_PERCENT in force for this report."
},
"days": {
"type": "array",
"items": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"assets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"assetCode": {
"type": "string"
},
"assetIssuer": {
"type": "string",
"nullable": true
},
"quotes": {
"type": "integer"
},
"feesQuoted": {
"type": "string",
"description": "Sum of every fee quoted — what clients were told to charge."
},
"alarms": {
"type": "integer"
},
"policyIds": {
"type": "array",
"items": {
"type": "string"
}
},
"collected": {
"type": "string",
"nullable": true,
"description": "What the fee destination received in this asset that day, per the index. Null when the asset is not indexed — which is not zero."
},
"collectedNote": {
"type": "string"
}
},
"required": [
"assetCode",
"assetIssuer",
"quotes",
"feesQuoted",
"alarms",
"policyIds",
"collected"
]
}
}
},
"required": [
"date",
"assets"
]
}
},
"policies": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"firstSeenAt": {
"type": "string"
},
"policy": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"flat",
"percent"
]
},
"assetCode": {
"type": "string"
},
"assetIssuer": {
"type": "string",
"nullable": true
},
"destination": {
"type": "string",
"description": "The fee destination — the address `/fees` already publishes."
},
"percent": {
"type": "string",
"nullable": true
},
"min": {
"type": "string",
"nullable": true
},
"max": {
"type": "string",
"nullable": true
},
"amount": {
"type": "string",
"nullable": true,
"description": "Flat mode only: the fixed fee."
}
},
"required": [
"mode",
"assetCode",
"assetIssuer",
"destination",
"percent",
"min",
"max",
"amount"
]
}
},
"required": [
"firstSeenAt",
"policy"
]
}
}
},
"required": [
"network",
"from",
"to",
"alarmPercent",
"days",
"policies"
]
}
}
}
},
"400": {
"description": "Bad date range",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
},
"401": {
"description": "Missing or wrong report token",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
},
"500": {
"description": "The fee ledger could not be read",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
},
"503": {
"description": "Fee reporting is not configured on this deployment",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
]
}
}
}
}
}
}
}
}
}
52 changes: 52 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -495,3 +495,55 @@ model NgnOrder {
@@index([network, status])
@@schema("wraith")
}

// ─── Platform fee ledger ─────────────────────────────────────────────────────

/// One fee policy for one asset, as `GET /fees` applied it.
///
/// Keyed by a hash of the policy itself, so the same configuration is the same
/// row however many quotes it prices, and any change — a new percent, floor,
/// cap, flat amount or destination — is a new row with its own `firstSeenAt`.
/// That is what lets a fee be explained after the fact: every `FeeQuote` names
/// the policy that produced it, and this table says when that policy appeared.
model FeePolicy {
/// sha256 of the canonical policy JSON, hex, first 32 characters.
id String @id
/// `{ mode, assetCode, assetIssuer, destination, percent, min, max, amount }`
/// — everything that decides a fee, and nothing about who asked.
policy Json
firstSeenAt DateTime @default(now())

@@schema("wraith")
}

/// One fee quoted by `GET /fees`, per asset.
///
/// **Nothing here identifies a person.** No IP, no user agent, no sender
/// address — the request carries none, and none is taken. The only address
/// is the fee destination, held inside the policy, which `/fees` publishes to
/// anyone who asks. The amount and the time are what a quote *is*; neither
/// names who wanted it.
model FeeQuote {
id Int @id @default(autoincrement())
network String
assetCode String
/// Null for native XLM.
assetIssuer String?
/// The amount priced against, in whole units of the asset. Null when the
/// client sent none — an old client, which is told the floor.
quotedAgainst String?
/// What the client was told to charge.
fee Decimal @db.Decimal(38, 7)
/// Which rule decided the fee: percent | floor | cap | flat | no_amount.
basis String
/// The fee as a percentage of `quotedAgainst`. Null without an amount.
feePercent Decimal? @db.Decimal(20, 7)
/// The fee exceeded VEIL_FEE_ALARM_PERCENT of the amount.
alarm Boolean @default(false)
policyId String
quotedAt DateTime @default(now())

@@index([network, quotedAt])
@@index([network, alarm, quotedAt])
@@schema("wraith")
}
Loading
Loading