Skip to content

feat: Platform fee rules — configurable per-transaction-type markup#204

Open
jaymantri wants to merge 2 commits intomainfrom
feat/platform-fee-rules
Open

feat: Platform fee rules — configurable per-transaction-type markup#204
jaymantri wants to merge 2 commits intomainfrom
feat/platform-fee-rules

Conversation

@jaymantri
Copy link

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)

Layer Source Configurable by platform?
Network/rail fees Counterparty institution No
Lightspark fees Grid infrastructure No
Platform markup This PR Yes

New endpoints

Method Path Description
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/disabled)
DELETE /fee-rules/{feeRuleId} Remove a rule
GET /fee-rules/report Monthly aggregated fee revenue by txn type

New schemas

  • FeeRule — the core entity with transactionType, feeType (FIXED / PERCENTAGE / HYBRID), fixedFee, variableFeeRate, enabled
  • FeeTransactionType — enum: TRANSFER_IN, TRANSFER_OUT, RAMP_ON, RAMP_OFF, CROSS_BORDER_PAYOUT
  • FeeRuleType — enum: FIXED, PERCENTAGE, HYBRID
  • FeeRuleCreateRequest / FeeRulePatchRequest — input schemas with validation constraints
  • FeeReport / FeeReportLine — monthly revenue aggregation

Extended existing schemas

OutgoingRateDetails and IncomingRateDetails gain 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 combination
  • INVALID_MONTH_FORMAT (400) — month parameter not in YYYY-MM format
  • FEE_RULE_NOT_FOUND (404)
  • FEE_RULE_EXISTS (409) — duplicate transaction type

Documentation

  • Shared Mintlify snippet (snippets/platform-fee-rules.mdx) with layering explanation, cURL examples, and constraints
  • Wrapper pages in all four use-case tabs (Payouts & B2B, Ramps, Rewards, Global P2P) under Platform Tools
  • API reference auto-generated from OpenAPI spec

Design decisions

  1. One rule per transaction type — keeps the model simple and deterministic; no priority/ordering complexity
  2. variableFeeRate as a double — matches existing gridApiVariableFeeRate convention (not basis points)
  3. fixedFee in cents (int64) — matches existing gridApiFixedFee convention
  4. No pagination on list — max 5 rules (bounded by transaction type enum), pagination is unnecessary overhead
  5. feeType immutable on PATCH — avoids ambiguity about which fields become required/optional mid-lifecycle; delete and recreate instead
  6. Report endpoint under /fee-rules/report — colocated with rules since it's platform fee revenue, not general transaction analytics
  7. USD-only for now — fee collection currency matches the PRD; schema includes currency field for future multi-currency support

Test plan

  • npm run lint passes (Redocly validation confirmed clean)
  • npm run build:openapi bundles successfully
  • Verify new endpoints appear in Mintlify API Reference tab
  • Verify "Platform fee rules" guide renders correctly under Platform Tools in all four use-case tabs
  • Review schema examples render properly in Mintlify
  • Confirm rateDetails extensions don't break existing quote/transaction examples

Made with Cursor

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>
@github-actions
Copy link

github-actions bot commented Feb 18, 2026

✱ Stainless preview builds

This PR will update the grid SDKs with the following commit messages.

kotlin

feat(api): add fee rules endpoints, update OutgoingRateDetails/IncomingTransaction types

openapi

feat(api): add fee_rules endpoints, types, platform fee fields to quote details

python

feat(api): add fee_rules endpoints, platform fee fields to transaction types

typescript

feat(api): add fee_rules resource, platform fee fields to quotes/transactions

Edit this comment to update them. They will appear in their respective SDK's changelogs.

⚠️ grid-typescript studio · code · diff

There was a regression in your SDK.
generate ❗build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/grid-typescript/67787fdebc420ac0352a587461f6cf78ddc7cfbc/dist.tar.gz
New diagnostics (1 warning)
⚠️ Method/PaginatedWithoutMatchingScheme: Skipped pagination for method `get /fee-rules` (no matching pagination scheme).
⚠️ 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.whl
New 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-apps
Copy link
Contributor

greptile-apps bot commented Feb 18, 2026

Greptile Summary

Adds 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 IncomingRateDetails and OutgoingRateDetails with three new optional platform fee fields, adds a monthly fee revenue report endpoint, and includes Mintlify documentation across all four use-case tabs via a shared snippet.

  • New endpoints: POST/GET /fee-rules, GET /fee-rules/report, GET/PATCH/DELETE /fee-rules/{feeRuleId} — well-structured with proper auth, error responses, and examples
  • Schema design: Clean separation between create/patch request schemas and response schema; validation constraints (fee caps, rate ranges) are consistent across request schemas
  • Documentation: Shared snippet pattern avoids duplication; wrapper pages have correct frontmatter for each use-case tab
  • Minor inconsistency: FeeRule.yaml response schema has maximum on variableFeeRate but not on fixedFee, unlike both request schemas which cap both
  • Description accuracy: FeeRule.variableFeeRate says "Applied to the sending amount" which is inaccurate for incoming transaction types where it applies to the receiving amount

Confidence Score: 4/5

  • This PR is safe to merge — it adds new API surface area with no breaking changes to existing schemas.
  • Well-designed OpenAPI spec addition with proper CRUD endpoints, validation constraints, error codes, and documentation. The only issues are minor inconsistencies in the response schema (missing maximum on fixedFee, slightly inaccurate variableFeeRate description). These are documentation-level concerns that won't cause runtime issues.
  • openapi/components/schemas/fee_rules/FeeRule.yaml has a constraint inconsistency and a description inaccuracy worth addressing before API consumers build against the spec.

Important Files Changed

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[]}
Loading

Last reviewed commit: 228a292

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

24 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 131 to 149
{
"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"
}
]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{
"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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Comment on lines 94 to 99
/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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
/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>
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

24 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +35 to +38
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +24 to +30
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments