feat: Platform fee rules — configurable per-transaction-type markup#204
feat: Platform fee rules — configurable per-transaction-type markup#204
Conversation
Introduce a complete CRUD API for platform-configurable fee markup,
enabling platforms to define per-transaction-type fees that layer on
top of Lightspark and counterparty fees.
New endpoints:
- POST /fee-rules — create a fee rule (one per transaction type)
- GET /fee-rules — list all fee rules with optional filters
- GET /fee-rules/{feeRuleId} — retrieve a single rule
- PATCH /fee-rules/{feeRuleId} — partial update (amounts, enabled)
- DELETE /fee-rules/{feeRuleId} — remove a rule
- GET /fee-rules/report — monthly aggregated fee revenue
New schemas:
- FeeRule, FeeTransactionType, FeeRuleType
- FeeRuleCreateRequest, FeeRulePatchRequest
- FeeReport, FeeReportLine
Extended existing schemas:
- OutgoingRateDetails and IncomingRateDetails gain platform fee
fields (platformFixedFee, platformVariableFeeRate,
platformVariableFeeAmount) so quotes and transactions surface the
platform markup alongside Lightspark fees.
New error codes:
- INVALID_FEE_RULE (400), INVALID_MONTH_FORMAT (400)
- FEE_RULE_NOT_FOUND (404), FEE_RULE_EXISTS (409)
Documentation:
- Shared snippet with guide, cURL examples, and fee layering table
- Wrapper pages in all four use-case tabs (Payouts, Ramps, Rewards,
Global P2P) under Platform Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
✱ Stainless preview buildsThis PR will update the kotlin openapi python typescript Edit this comment to update them. They will appear in their respective SDK's changelogs.
|
⚠️ grid-openapi studio · code · diff
There was a regression in your SDK.
generate ❗New diagnostics (1 warning)
⚠️ Method/PaginatedWithoutMatchingScheme: Skipped pagination for method `get /fee-rules` (no matching pagination scheme).
⚠️ grid-python studio · code · diff
There was a regression in your SDK.
generate ❗→build ✅→lint ✅→test ✅pip install https://pkg.stainless.com/s/grid-python/011b52950358bd4a4c05a06033135ebf2153617a/grid-0.0.1-py3-none-any.whlNew diagnostics (1 warning)
⚠️ Method/PaginatedWithoutMatchingScheme: Skipped pagination for method `get /fee-rules` (no matching pagination scheme).
⚠️ grid-kotlin studio · code · diff
There was a regression in your SDK.
generate ❗→build ✅→lint ✅→test ✅New diagnostics (1 warning)
⚠️ Method/PaginatedWithoutMatchingScheme: Skipped pagination for method `get /fee-rules` (no matching pagination scheme).
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-02-18 17:21:04 UTC
Greptile SummaryAdds a complete CRUD API for platform-configurable fee rules, enabling Grid platforms to define per-transaction-type markup (fixed, percentage, or hybrid) that layers on top of Lightspark and counterparty fees. The PR also extends
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| openapi/openapi.yaml | Adds Platform Fee Rules tag and new /fee-rules, /fee-rules/report, /fee-rules/{feeRuleId} path refs. Path ordering places /fee-rules/report after the parameterized path (already noted in prior thread). |
| openapi/components/schemas/fee_rules/FeeRule.yaml | Core FeeRule response schema. fixedFee is missing maximum constraint while variableFeeRate has one. variableFeeRate description says "Applied to the sending amount" which is inaccurate for incoming transactions. |
| openapi/components/schemas/fee_rules/FeeRuleCreateRequest.yaml | Well-structured create request with proper validation constraints on both fixedFee (max 10000) and variableFeeRate (max 0.20). Defaults enabled to true. |
| openapi/components/schemas/fee_rules/FeeRulePatchRequest.yaml | Patch request with no required fields (correct for PATCH semantics). Validation constraints match create request. |
| openapi/paths/fee-rules/fee_rules.yaml | POST (create) and GET (list) endpoints with proper auth, error responses, examples, and filter params. Well-structured. |
| openapi/paths/fee-rules/fee_rules_{feeRuleId}.yaml | GET, PATCH, DELETE operations for individual fee rules. Proper error responses and examples for each operation. |
| openapi/paths/fee-rules/fee_rules_report.yaml | Monthly fee report endpoint with optional month query parameter. Example response totals are consistent (62500 + 51300 + 14650 = 128450). |
| openapi/components/schemas/transactions/IncomingRateDetails.yaml | Adds three new optional platform fee fields. Descriptions correctly reference "receiving amount" and "receiving currency" consistent with existing gridApi* fields. |
| openapi/components/schemas/transactions/OutgoingRateDetails.yaml | Adds three new optional platform fee fields. Descriptions correctly reference "sending amount" and "sending currency" consistent with existing gridApi* fields. |
| mintlify/snippets/platform-fee-rules.mdx | Shared doc snippet with fee rules guide. Well-structured with Steps, tables, and examples. Fee report example totals now correct (post prior-thread fix). |
Sequence Diagram
sequenceDiagram
participant P as Platform
participant G as Grid API
participant DB as Fee Rules Store
Note over P,DB: Fee Rule Configuration
P->>G: POST /fee-rules {transactionType, feeType, fixedFee, variableFeeRate}
G->>DB: Store fee rule (one per txn type)
DB-->>G: FeeRule created
G-->>P: 201 FeeRule
Note over P,DB: Quote with Platform Fee
P->>G: POST /quotes (for transaction type with active rule)
G->>DB: Lookup active fee rule for txn type
DB-->>G: FeeRule {fixedFee, variableFeeRate}
G-->>P: Quote with rateDetails including platformFixedFee, platformVariableFeeRate, platformVariableFeeAmount
Note over P,DB: Fee Revenue Reporting
P->>G: GET /fee-rules/report?month=2026-02
G->>DB: Aggregate settled fees by txn type
DB-->>G: FeeReport lines
G-->>P: 200 FeeReport {totalPlatformFeesCollected, lines[]}
Last reviewed commit: 228a292
| { | ||
| "month": "2026-02", | ||
| "totalPlatformFeesCollected": 128450, | ||
| "currency": "USD", | ||
| "lines": [ | ||
| { | ||
| "transactionType": "CROSS_BORDER_PAYOUT", | ||
| "transactionCount": 97, | ||
| "platformFeesCollected": 51300, | ||
| "currency": "USD" | ||
| }, | ||
| { | ||
| "transactionType": "TRANSFER_OUT", | ||
| "transactionCount": 245, | ||
| "platformFeesCollected": 62500, | ||
| "currency": "USD" | ||
| } | ||
| ] | ||
| } |
There was a problem hiding this comment.
Fee report example total doesn't match line items
The totalPlatformFeesCollected is 128,450 but the two line items sum to only 113,800 (51,300 + 62,500). The OpenAPI example in fee_rules_report.yaml includes a third RAMP_OFF line (14,650) that makes the total correct — but the documentation snippet is missing that line.
Either add the missing RAMP_OFF line to match the total, or adjust totalPlatformFeesCollected to 113,800.
| { | |
| "month": "2026-02", | |
| "totalPlatformFeesCollected": 128450, | |
| "currency": "USD", | |
| "lines": [ | |
| { | |
| "transactionType": "CROSS_BORDER_PAYOUT", | |
| "transactionCount": 97, | |
| "platformFeesCollected": 51300, | |
| "currency": "USD" | |
| }, | |
| { | |
| "transactionType": "TRANSFER_OUT", | |
| "transactionCount": 245, | |
| "platformFeesCollected": 62500, | |
| "currency": "USD" | |
| } | |
| ] | |
| } | |
| ```json 200 OK | |
| { | |
| "month": "2026-02", | |
| "totalPlatformFeesCollected": 128450, | |
| "currency": "USD", | |
| "lines": [ | |
| { | |
| "transactionType": "CROSS_BORDER_PAYOUT", | |
| "transactionCount": 97, | |
| "platformFeesCollected": 51300, | |
| "currency": "USD" | |
| }, | |
| { | |
| "transactionType": "TRANSFER_OUT", | |
| "transactionCount": 245, | |
| "platformFeesCollected": 62500, | |
| "currency": "USD" | |
| }, | |
| { | |
| "transactionType": "RAMP_OFF", | |
| "transactionCount": 58, | |
| "platformFeesCollected": 14650, | |
| "currency": "USD" | |
| } | |
| ] | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/platform-fee-rules.mdx
Line: 131-149
Comment:
**Fee report example total doesn't match line items**
The `totalPlatformFeesCollected` is 128,450 but the two line items sum to only 113,800 (51,300 + 62,500). The OpenAPI example in `fee_rules_report.yaml` includes a third `RAMP_OFF` line (14,650) that makes the total correct — but the documentation snippet is missing that line.
Either add the missing `RAMP_OFF` line to match the total, or adjust `totalPlatformFeesCollected` to 113,800.
```suggestion
```json 200 OK
{
"month": "2026-02",
"totalPlatformFeesCollected": 128450,
"currency": "USD",
"lines": [
{
"transactionType": "CROSS_BORDER_PAYOUT",
"transactionCount": 97,
"platformFeesCollected": 51300,
"currency": "USD"
},
{
"transactionType": "TRANSFER_OUT",
"transactionCount": 245,
"platformFeesCollected": 62500,
"currency": "USD"
},
{
"transactionType": "RAMP_OFF",
"transactionCount": 58,
"platformFeesCollected": 14650,
"currency": "USD"
}
]
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Unused import: gridBaseUrl
gridBaseUrl is imported but never used in this file — the cURL examples hardcode https://api.lightspark.com/grid/2025-10-13 directly. This is consistent with other snippet files (none of them actually use gridBaseUrl in code blocks), so consider removing the unused import.
| import { gridBaseUrl } from '/snippets/variables.mdx'; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/platform-fee-rules.mdx
Line: 1
Comment:
**Unused import: `gridBaseUrl`**
`gridBaseUrl` is imported but never used in this file — the cURL examples hardcode `https://api.lightspark.com/grid/2025-10-13` directly. This is consistent with other snippet files (none of them actually use `gridBaseUrl` in code blocks), so consider removing the unused import.
```suggestion
```
How can I resolve this? If you propose a fix, please make it concise.| /fee-rules: | ||
| $ref: paths/fee-rules/fee_rules.yaml | ||
| /fee-rules/{feeRuleId}: | ||
| $ref: paths/fee-rules/fee_rules_{feeRuleId}.yaml | ||
| /fee-rules/report: | ||
| $ref: paths/fee-rules/fee_rules_report.yaml |
There was a problem hiding this comment.
Literal path /fee-rules/report ordered after parameterized /fee-rules/{feeRuleId}
/fee-rules/{feeRuleId} appears before /fee-rules/report. While OpenAPI 3.x says path ordering doesn't determine routing, many code generators and API gateways use first-match-wins semantics — which means /fee-rules/report could be matched by the {feeRuleId} parameter, with "report" captured as the ID.
This is the same pattern already present with /customers/{customerId} before /customers/kyc-link, so it may be intentional for this repo. But if any consumer relies on YAML ordering, swapping the literal path above the parameterized one would be safer:
| /fee-rules: | |
| $ref: paths/fee-rules/fee_rules.yaml | |
| /fee-rules/{feeRuleId}: | |
| $ref: paths/fee-rules/fee_rules_{feeRuleId}.yaml | |
| /fee-rules/report: | |
| $ref: paths/fee-rules/fee_rules_report.yaml | |
| /fee-rules: | |
| $ref: paths/fee-rules/fee_rules.yaml | |
| /fee-rules/report: | |
| $ref: paths/fee-rules/fee_rules_report.yaml | |
| /fee-rules/{feeRuleId}: | |
| $ref: paths/fee-rules/fee_rules_{feeRuleId}.yaml |
Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/openapi.yaml
Line: 94-99
Comment:
**Literal path `/fee-rules/report` ordered after parameterized `/fee-rules/{feeRuleId}`**
`/fee-rules/{feeRuleId}` appears before `/fee-rules/report`. While OpenAPI 3.x says path ordering doesn't determine routing, many code generators and API gateways use first-match-wins semantics — which means `/fee-rules/report` could be matched by the `{feeRuleId}` parameter, with `"report"` captured as the ID.
This is the same pattern already present with `/customers/{customerId}` before `/customers/kyc-link`, so it may be intentional for this repo. But if any consumer relies on YAML ordering, swapping the literal path above the parameterized one would be safer:
```suggestion
/fee-rules:
$ref: paths/fee-rules/fee_rules.yaml
/fee-rules/report:
$ref: paths/fee-rules/fee_rules_report.yaml
/fee-rules/{feeRuleId}:
$ref: paths/fee-rules/fee_rules_{feeRuleId}.yaml
```
How can I resolve this? If you propose a fix, please make it concise.- Rename operationId from listFeeRules to getFeeRules to avoid
Stainless auto-detecting the endpoint as paginated (matches the
getExchangeRates convention for non-paginated collections)
- Reorder paths: /fee-rules/report before /fee-rules/{feeRuleId}
so the literal path precedes the parameterized one
- Add missing RAMP_OFF line item to the doc snippet fee report
example so the total (128,450) matches the sum of line items
- Remove unused gridBaseUrl import from the snippet
Co-authored-by: Cursor <cursoragent@cursor.com>
| description: >- | ||
| Variable fee as a decimal rate of the transaction amount | ||
| (e.g., 0.005 = 0.5%). Applied to the sending amount at quote creation. | ||
| Required when feeType is PERCENTAGE or HYBRID. |
There was a problem hiding this comment.
variableFeeRate description is misleading for incoming transactions
The description says "Applied to the sending amount at quote creation," but this is only accurate for outgoing transactions. For incoming transactions (TRANSFER_IN, RAMP_ON), the IncomingRateDetails.platformVariableFeeAmount description says it's applied to the receiving amount — matching how the existing gridApiVariableFeeRate works in that context.
Consider using direction-neutral language like "Applied to the transaction amount at quote creation" to avoid confusing consumers who read both this schema and the rate details.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/fee_rules/FeeRule.yaml
Line: 35-38
Comment:
**`variableFeeRate` description is misleading for incoming transactions**
The description says "Applied to the sending amount at quote creation," but this is only accurate for outgoing transactions. For incoming transactions (`TRANSFER_IN`, `RAMP_ON`), the `IncomingRateDetails.platformVariableFeeAmount` description says it's applied to the *receiving* amount — matching how the existing `gridApiVariableFeeRate` works in that context.
Consider using direction-neutral language like "Applied to the transaction amount at quote creation" to avoid confusing consumers who read both this schema and the rate details.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| fixedFee: | ||
| type: integer | ||
| format: int64 | ||
| description: >- | ||
| Fixed fee in the smallest unit of USD (cents). Applied per transaction. | ||
| Required when feeType is FIXED or HYBRID. | ||
| minimum: 0 |
There was a problem hiding this comment.
fixedFee missing maximum constraint in response schema
The FeeRuleCreateRequest and FeeRulePatchRequest schemas both specify maximum: 10000 for fixedFee, but this response schema omits it. Meanwhile, variableFeeRate (line 40) does have maximum: 0.20 here. Code generators that derive validation from the response schema (e.g., for round-trip testing or SDK type constraints) would not enforce the $100 cap on this field.
| fixedFee: | |
| type: integer | |
| format: int64 | |
| description: >- | |
| Fixed fee in the smallest unit of USD (cents). Applied per transaction. | |
| Required when feeType is FIXED or HYBRID. | |
| minimum: 0 | |
| fixedFee: | |
| type: integer | |
| format: int64 | |
| description: >- | |
| Fixed fee in the smallest unit of USD (cents). Applied per transaction. | |
| Required when feeType is FIXED or HYBRID. | |
| minimum: 0 | |
| maximum: 10000 | |
| example: 150 |
Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/fee_rules/FeeRule.yaml
Line: 24-30
Comment:
**`fixedFee` missing `maximum` constraint in response schema**
The `FeeRuleCreateRequest` and `FeeRulePatchRequest` schemas both specify `maximum: 10000` for `fixedFee`, but this response schema omits it. Meanwhile, `variableFeeRate` (line 40) does have `maximum: 0.20` here. Code generators that derive validation from the response schema (e.g., for round-trip testing or SDK type constraints) would not enforce the $100 cap on this field.
```suggestion
fixedFee:
type: integer
format: int64
description: >-
Fixed fee in the smallest unit of USD (cents). Applied per transaction.
Required when feeType is FIXED or HYBRID.
minimum: 0
maximum: 10000
example: 150
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
Adds a complete CRUD API for platform-configurable fee rules, enabling Grid platforms to define per-transaction-type markup that layers on top of Lightspark and counterparty fees.
This closes the last major gap in the Grid pricing model: the API already surfaces fee fields on quotes and transactions (
gridApiFixedFee,gridApiVariableFeeRate,gridApiVariableFeeAmount), but platforms had no way to configure their own markup. This PR adds that capability.Fee architecture (three layers)
New endpoints
POST/fee-rulesGET/fee-rulesGET/fee-rules/{feeRuleId}PATCH/fee-rules/{feeRuleId}DELETE/fee-rules/{feeRuleId}GET/fee-rules/reportNew schemas
transactionType,feeType(FIXED / PERCENTAGE / HYBRID),fixedFee,variableFeeRate,enabledTRANSFER_IN,TRANSFER_OUT,RAMP_ON,RAMP_OFF,CROSS_BORDER_PAYOUTFIXED,PERCENTAGE,HYBRIDExtended existing schemas
OutgoingRateDetailsandIncomingRateDetailsgain three new optional fields so quotes and transactions surface platform markup alongside Lightspark fees:platformFixedFee(int64, cents)platformVariableFeeRate(double, e.g. 0.005 = 0.5%)platformVariableFeeAmount(int64, cents, calculated)New error codes
INVALID_FEE_RULE(400) — missing fields, amounts out of range, or wrong feeType/field combinationINVALID_MONTH_FORMAT(400) — month parameter not in YYYY-MM formatFEE_RULE_NOT_FOUND(404)FEE_RULE_EXISTS(409) — duplicate transaction typeDocumentation
snippets/platform-fee-rules.mdx) with layering explanation, cURL examples, and constraintsDesign decisions
variableFeeRateas adouble— matches existinggridApiVariableFeeRateconvention (not basis points)fixedFeein cents (int64) — matches existinggridApiFixedFeeconventionfeeTypeimmutable on PATCH — avoids ambiguity about which fields become required/optional mid-lifecycle; delete and recreate instead/fee-rules/report— colocated with rules since it's platform fee revenue, not general transaction analyticscurrencyfield for future multi-currency supportTest plan
npm run lintpasses (Redocly validation confirmed clean)npm run build:openapibundles successfullyrateDetailsextensions don't break existing quote/transaction examplesMade with Cursor