Skip to content

docs(spec): note that exchangeRate is fee-exclusive - #888

Open
pengying wants to merge 1 commit into
mainfrom
09-01-docs_spec_note_that_exchangerate_is_fee-exclusive
Open

docs(spec): note that exchangeRate is fee-exclusive#888
pengying wants to merge 1 commit into
mainfrom
09-01-docs_spec_note_that_exchangerate_is_fee-exclusive

Conversation

@pengying

@pengying pengying commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Documents that exchangeRate is fee-exclusive, on all three schemas that expose it: Quote, Transaction, and ExchangeRate.

The previous description was Number of sending currency units per receiving currency unit. That is silent on where fees sit relative to the rate, so an integrator converting a sending amount at the quoted rate computes a receiving amount that is too high by the fee.

Verification

The rate is computed in grid/objects/quote.py:

Decimal(sending_amount - total_fees)
* Decimal(10**receiving_currency.decimals)
/ (Decimal(receiving_amount) * Decimal(10**sending_currency.decimals))

Fees are deducted before the conversion. Checked against the published examples whose rates are precise enough to be unambiguous:

Example send fee recv published rate computed
quote-system.mdx USD→EUR 100000 500 91540 1.08695652 1.08695652
currencies-and-rails.mdx USD→MXN 100000 500 1716375 0.05797101 0.05797101
quote-system.mdx USD→BTC 100000 500 828835 120048.01920768 120048.01920768

Wording

Each description is written for the fields available on its own object:

  • Quote names feesIncluded and totalSendingAmount directly.
  • Transaction states the property without naming fields. The object carries no fee field to reference.
  • ExchangeRate points at the sibling fees object.

Each states the fee ordering and stops there, leaving the arithmetic to the reader.

Test plan

  • make build rebundled openapi.yaml and mintlify/openapi.yaml. The bundle diff is exactly the three description lines.
  • @redocly/cli@1.34.5 lint openapi.yaml: valid.
  • spectral lint --fail-severity=error: 0 errors. Remaining warnings and infos are pre-existing across the spec.

Follow-ups, not addressed here

Two pre-existing issues surfaced while verifying this change. Both are worth fixing, and neither belongs in a description-only PR.

1. The rate's unit basis is undocumented. The amount fields are in the smallest unit of their currency, but the rate is not: each side is scaled by its currency's decimals first. For USD→BTC, dividing the wire integers (99500 cents / 828835 sats) gives 0.12004802, off by 10^6 from the published 120048.01920768; scaling each side first (995.00 / 0.00828835) reproduces it exactly. Same-decimal corridors hide this, so it only bites on pairs like USD→BTC. Wording that explains this clearly, without leaning on "major/minor unit" (terms absent from this spec) or on named denominations (BTC's smallest unit is the satoshi), needs its own pass.

2. Most quote examples in the repo contradict this field. 16 of the 18 quote examples do not satisfy the documented relationship between exchangeRate, the amounts, and the fee. Most publish the rate inverted, receiving-per-sending rather than sending-per-receiving. openapi/paths/quotes/quotes.yaml shows exchangeRate: 0.92 for a USD→EUR send where sending-per-receiving is about 1.087, so 10000 x 0.92 = 9200 reconciles only because the rate direction and the operation are both flipped and the errors cancel. Only quote-system.mdx and currencies-and-rails.mdx are correct today.

A reader who checks this description against a nearby example will find they disagree, so fixing the examples is the higher-value follow-up.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
grid-flow-builder Ignored Ignored Preview Sep 2, 2026 6:46am UTC
grid-wallet-demo Ignored Ignored Preview Sep 2, 2026 6:46am UTC

Request Review

@pengying
pengying marked this pull request as ready for review September 1, 2026 21:56

pengying commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

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

cli

chore(internal): regenerate SDK with no functional changes

go

docs(types): clarify exchangeRate fee handling in quote/transaction/exchange types

kotlin

docs(types): clarify exchangeRate field is fee-exclusive across models

openapi

docs(api): clarify exchangeRate field calculation and fee handling

php

docs(types): update exchangeRate field docs in ExchangeRate/Quote/Transaction/Webhook

python

docs(types): clarify exchange_rate is fee-exclusive in quotes/transactions

ruby

docs(api): clarify exchange_rate is fee-exclusive in quote/transaction/exchange_rate models

typescript

docs(api): clarify exchangeRate is fee-exclusive across resources

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

grid-typescript studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ❗test ✅

npm install https://pkg.stainless.com/s/grid-typescript/f925fbe41dc4f2f19e354f7549ec8db58cd7bad1/dist.tar.gz
grid-openapi studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️

grid-ruby studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ✅test ✅

grid-kotlin studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️build ✅lint ✅test ❗

grid-go studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@977b2e49e0131c9ed540dc0f99764bfb71f32468
grid-python studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/9a40a70b829368d13ef42b29731b8cf71e0c5877/grid-0.0.1-py3-none-any.whl
grid-php studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅lint ✅test ✅

grid-cli studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️build ❗lint ❗test ❗


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-09-02 06:52:19 UTC

@mintlify

mintlify Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Sep 1, 2026, 10:00 PM

@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR clarifies that exchange rates exclude fees across exchange-rate, quote, and transaction schemas, then propagates those descriptions into the assembled OpenAPI specifications. However, the new quote and transaction arithmetic conflicts with existing published examples.

  • Documents fees as separate from currency conversion.
  • Adds an explicit fee-adjusted quote formula.
  • Synchronizes modular schema descriptions into both bundled specifications.

Confidence Score: 4/5

The exchange-rate arithmetic should be reconciled with the published quote and transaction examples before merging so clients are not given contradictory calculation guidance.

The changed quote formula prescribes fee subtraction followed by division, while existing quote and transaction examples consistently obtain receiving amounts by multiplying by the documented rate.

Files Needing Attention: openapi/components/schemas/quotes/Quote.yaml and openapi/components/schemas/transactions/Transaction.yaml

Important Files Changed

Filename Overview
openapi/components/schemas/exchange_rates/ExchangeRate.yaml Clarifies fee-exclusive exchange-rate semantics; the intentional exchange-rate example arithmetic was not flagged.
openapi/components/schemas/quotes/Quote.yaml Adds an explicit fee-adjusted division formula that conflicts with multiple published quote examples using multiplication.
openapi/components/schemas/transactions/Transaction.yaml Adds fee-exclusive wording and rejects multiplication, conflicting with existing transaction examples that use the inverse rate convention.
openapi.yaml Propagates the three schema-description changes into the assembled public contract, including the inconsistent quote and transaction guidance.
mintlify/openapi.yaml Propagates the same descriptions into the documentation contract, exposing the inconsistent arithmetic to documentation consumers.
Prompt To Fix All With AI
### Issue 1
openapi/components/schemas/quotes/Quote.yaml:83-88
**Exchange-rate formula contradicts examples**

When an integrator derives `totalReceivingAmount` from a quote, the new formula computes `(10000 - 10) / 0.92 ≈ 10858.7`, while the published example returns `9200` using `10000 × 0.92`. This contradictory rate direction causes clients following the description to calculate and display a different receiving amount from the API response; the same inconsistency affects the new transaction wording.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "docs(spec): note that exchangeRate is fe..." | Re-trigger Greptile

Comment thread openapi/components/schemas/quotes/Quote.yaml Outdated
@pengying

pengying commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — there is a real contradiction here, but it is not in the new wording. The quotes.yaml example is wrong, and it was wrong before this PR.

The rate direction is inverted in that example. exchangeRate has always been documented as "Number of sending currency units per receiving currency unit" (unchanged by this PR). For USD to EUR at roughly 0.92 EUR/USD, sending-per-receiving is USD per EUR, which is about 1.087. The example publishes 0.92, which is the inverse, EUR per USD. So 10000 x 0.92 = 9200 only "works" because both the rate and the operation are flipped, and two errors cancel.

Examples with precise rates confirm the documented direction and the fee-exclusive formula:

Example send fee recv published rate (send - fee) / recv
quote-system.mdx USD→EUR 100000 500 91540 1.08695652 1.08695652
currencies-and-rails.mdx USD→MXN 100000 500 1716375 0.05797101 0.05797101
quotes.yaml USD→EUR 10000 10 9200 0.92 1.08586957 ❌

The two that match to eight significant figures are unambiguous: a round number like 0.92 can be coincidence, but 0.05797101 and 1.08695652 cannot. Both are USD to EUR/MXN sends, and both agree with the implementation in sparkcore, grid/utils/exchange_rate.py:

return float(Decimal(sending_amount - fees) / Decimal(receiving_amount))

On the Transaction change: it adds no formula, only the statement that the rate is fee-exclusive. That is accurate regardless of direction.

Not fixing the example in this PR. Correcting quotes.yaml means changing a published response body, and the same inverted convention appears in roughly a dozen .mdx examples (send-payment.mdx, accounts.mdx, reconciliation.mdx, quickstart.mdx, and others), several of which also fail the fee-exclusive check independently. That is a separate, wider cleanup that deserves its own review rather than being bundled into a description-only change. Filing it as a follow-up.

@pengying
pengying force-pushed the 09-01-docs_spec_note_that_exchangerate_is_fee-exclusive branch from 743b1b4 to bf7a375 Compare September 1, 2026 22:29
@pengying

pengying commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment: chasing this further showed my Quote formula was itself wrong, so I have amended the PR.

What I got wrong. I wrote the formula in minor units, (totalSendingAmount - feesIncluded) / exchangeRate. That holds only when both currencies have the same number of decimals, which every example I checked happened to have. The USD→BTC example in quote-system.mdx breaks it: USD has 2 decimals, BTC has 8, and the minor-unit ratio gives 0.12004802 against a published 120048.01920768, off by 10^6.

The implementation in grid/objects/quote.py scales by decimals:

Decimal(sending_amount - total_fees)
* Decimal(10**receiving_currency.decimals)
/ (Decimal(receiving_amount) * Decimal(10**sending_currency.decimals))

So the rate is a ratio of major units. The descriptions now say "in major units" and describe the fee ordering in words rather than giving a minor-unit formula that is only conditionally true. Three examples now confirm it, including the cross-decimal one:

Example published rate computed
USD→EUR 1.08695652 1.08695652
USD→MXN 0.05797101 0.05797101
USD→BTC 120048.01920768 120048.01920768

On the original flag. The contradiction you identified is real, and the fee-exclusive claim still holds. The quotes.yaml example remains wrong independently: it publishes 0.92 for a USD→EUR send where sending-per-receiving is about 1.087, so 10000 x 0.92 = 9200 reconciles only because the rate direction and the operation are both inverted.

I audited every quote example in the repo: 16 of 18 fail the documented relationship, nearly all by publishing the rate inverted. Only quote-system.mdx and currencies-and-rails.mdx are correct. That is a pre-existing docs problem spanning many published response bodies, so it is going in a separate PR rather than this description-only change.

Good catch. The formula would have shipped wrong for cross-decimal corridors.

@pengying
pengying force-pushed the 09-01-docs_spec_note_that_exchangerate_is_fee-exclusive branch from 1c51d00 to 1199872 Compare September 2, 2026 06:46
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