feat: add Bitcoin L1 deposit-address instruction + refundAddress to quotes - #736
feat: add Bitcoin L1 deposit-address instruction + refundAddress to quotes#736peterrojs wants to merge 5 commits into
Conversation
Add a Bitcoin L1 on/off-ramp surface to the quote/execute flow: - New self-contained PaymentBitcoinDepositAddressInfo member in the PaymentInstructions oneOf (accountType=BITCOIN_L1 discriminator value, on-chain address, optional network fixed to BITCOIN). - Optional refundAddress on RealtimeFundingQuoteSource (on-ramp crypto funding) and AccountDestination (off-ramp crypto payout). The member is self-contained rather than composing BasePaymentAccountInfo, so it does not extend the shared PaymentAccountType enum; the discriminator mapping is the source of truth for the BITCOIN_L1 value. All additions are optional / additive, so no version bump. SDKs are regenerated by the Stainless CI action from the bundled spec + config. Co-Authored-By: peterrojs <peterrojs@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-typescript studio · code · diff
✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-python studio · code · diff
✅ grid-php studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
Greptile SummaryThis PR adds the public API types for Bitcoin L1 quote and execution flows. The main changes are:
Confidence Score: 5/5The API additions look mergeable after aligning the Bitcoin discriminator with the shared account-type contract.
openapi/components/schemas/common/PaymentBitcoinDepositAddressInfo.yaml and the shared PaymentAccountType schema
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/common/PaymentBitcoinDepositAddressInfo.yaml | Adds the Bitcoin L1 instruction but defines its discriminator outside the shared payment account-type enum. |
| openapi/components/schemas/common/PaymentInstructions.yaml | Adds the Bitcoin L1 schema to the instruction union and discriminator mapping. |
| openapi/components/schemas/quotes/RealtimeFundingQuoteSource.yaml | Adds an optional refund address for on-chain real-time funding. |
| openapi/components/schemas/quotes/AccountDestination.yaml | Adds an optional refund address for crypto payout destinations. |
| openapi.yaml | Updates the generated root bundle with the new public schema fields. |
| mintlify/openapi.yaml | Updates the Mintlify bundle to match the source OpenAPI changes. |
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
openapi/components/schemas/common/PaymentBitcoinDepositAddressInfo.yaml:7-12
**Discriminator Bypasses Shared Enum**
This defines `BITCOIN_L1` only in a local enum while the shared `PaymentAccountType` used by the other payment-info models does not include it. SDK generators or consumers that expose `accountType` through that shared enum cannot represent a valid Bitcoin L1 instruction, causing compile-time rejection or failed response deserialization.
Reviews (1): Last reviewed commit: "feat(quotes): add Bitcoin L1 deposit-add..." | Re-trigger Greptile
Revision — align Bitcoin L1 with the shared account-type contractAddressed Greptile's inline finding (Discriminator Bypasses Shared Enum) in
Net effect: generated SDK clients that surface |
847f94c to
699ff8c
Compare
Correction — reverted the Greptile "shared enum" changeMy previous revision comment is superseded. I applied Greptile's Discriminator Bypasses Shared Enum suggestion in Why it broke: moving Why the original design is right: Full reasoning is in the inline thread. No code change vs. what you published — CI should return to green. |
|
📌 Bolt Status 2026-07-24 20:29:53 UTC — ⚡ Agent |
Add BITCOIN_L1 to the shared PaymentAccountType enum and have PaymentBitcoinDepositAddressInfo inherit BasePaymentAccountInfo, narrowing accountType to BITCOIN_L1 as every other payment-instruction member does. Co-Authored-By: akanter <akanter@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Revision — Bitcoin L1 now modeled like its cousins (per @akanter)Implemented @akanter's call in
This supersedes my earlier "Correction" comment: I had reverted this change when it broke CI, laid out the trade-off, and @akanter confirmed he wants cousin-consistency — so it's back in. Expected CI red:
|
Co-Authored-By: peterrojs <peterrojs@users.noreply.github.com>
|
One correction I owe you and @akanter: I said earlier that the oasdiff detection step "passes because all findings are WARN." That was wrong, and your workflow comment names exactly why — It doesn't change the plan, but it does change the framing: the errors are Nothing outstanding from me: the schema is modeled the way @akanter asked, and Lint, Docs Build, breaking-changes, and mergeability are all green. |
|
Thanks! No action needed to loop them in — both are already requested reviewers on this PR (jklein24 and shreyav, alongside AaryamanBhute and pengying), and your mention pinged them directly. Current state for whoever picks it up:
One process note, since it may block the merge: there's no formal |
| refundAddress: | ||
| type: string | ||
| description: >- | ||
| Optional address the funds are returned to if the payout cannot be | ||
| completed. Applies to crypto off-ramp destinations. | ||
| example: bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq |
There was a problem hiding this comment.
The PR description is a little confusing - it says this is for offramps? For offramp refunds (e.g., USDB -> BTC offramp fails, so we want to refund) shouldn't that automatically just go back to the source, and would we ever not know that value?
I think it makes sense to me that this is not the case for onramps - if the flow fails I'm guessing you should be able to specify where your BTC should go back to if it doesn't work.
There was a problem hiding this comment.
The confusing description is on me — I rewrote that section and mislabeled it. Fixing it now. And I think you're right on the substance; here's what I found digging into the server side.
Why the wording reads wrong. AccountDestination covers every account destination, including fiat bank accounts, where a refund address is meaningless. Calling it "off-ramp" made it sound like it applies to the fiat-payout case. It only has any meaning when the destination account is a crypto one (BTC out).
On whether it's needed at all — I think not, for the reason you gave. For a BTC-out payout, Grid is the party funding the upstream swap, so a failed payout refunds to a Grid-controlled address Grid already knows; the caller has nothing to contribute. And in the case where there genuinely is an inbound on-chain leg whose sender we can't infer (customer deposits BTC/stables to a generated address), that's already covered by RealtimeFundingQuoteSource.refundAddress — the source side, which is exactly the asymmetry you described.
Where the field probably came from. The upstream Orchestra API requires refundAddress on both POST /v1/orchestration/quote and POST /v1/orchestration/submit — in sparkcore/sparkcore/grid/flashnet/orchestra_client.py it's a required kwarg on both and always written into the body, for every order regardless of direction. It's an easy step from "the upstream call always needs one" to "we need one per direction," but those aren't the same thing: the upstream needing a value doesn't mean the API caller has to supply it.
Nothing consumes it yet. That flashnet package currently has no non-test importers, and there's no BITCOIN_L1 handling in sparkcore at all — consistent with the integration still being on stopgaps. So dropping the field costs nothing today.
The cost is asymmetric, which is what tips it for me: removing it now is free, and adding it back later is additive and non-breaking. Shipping it and removing it later is a breaking change. Given it's unused and the semantics are dubious, the cheap and safe move is to drop AccountDestination.refundAddress and keep the source-side one.
@peterrojs — you wrote the Orchestra client, so you'd know the flow better than I can infer it: is there an off-ramp shape where the refund target isn't derivable from the source, e.g. the customer funds the swap directly rather than out of a Grid-held balance? If not, say the word and I'll drop the field and rebundle. I didn't want to remove something you added deliberately for work you're mid-way through wiring up.

Summary
Adds the public API surface for a Bitcoin L1 crypto on/off-ramp on the quote/execute flow. All changes are additive and optional, so no version bump.
Changes
PaymentBitcoinDepositAddressInfopayment instruction — a member of thePaymentInstructions.accountOrWalletInfooneOf with a newBITCOIN_L1discriminator value. Carries the on-chain depositaddressand an optionalnetwork(fixed toBITCOIN). Modeled exactly like its cousins: it inheritsBasePaymentAccountInfoand narrows the sharedaccountTypeto its discriminator value, withBITCOIN_L1added to the sharedPaymentAccountTypeenum.refundAddressonRealtimeFundingQuoteSource— where to return an inbound on-chain deposit that can't be routed to complete the payment. This is the case where the sender can't reliably be inferred from the deposit, so the caller supplies it.refundAddressonAccountDestination— intended for a crypto payout destination (not fiat accounts, where a refund address has no meaning). Under discussion — likely to be dropped: for a crypto payout the refund lands on a Grid-controlled address we already know, and any inbound on-chain leg is already covered by the source-side field. See this thread.BITCOINis already a validcryptoNetworkforRealtimeFundingQuoteSource(currency=BTC, cryptoNetwork=BITCOIN); no change needed.Notes
PaymentAccountTypeis referenced only viaBasePaymentAccountInfo, so the newBITCOIN_L1value surfaces only on payment-instruction members.breaking-changelabel: oasdiff reports 76 errors / 1742 warnings. The errors areresponse-property-one-of-added(the new oneOf member appearing in quote and transaction responses); the warnings are the newBITCOIN_L1enum value propagating to theaccountTypeof every member that inheritsBasePaymentAccountInfo. All are additive — no property or enum-value removals — so this is the expected shape for a new payment-instruction member, gated by API-reviewer approval per the repo's workflow design.Test plan
npm run lint:openapi(redocly bundle + redocly lint + spectral lint): passes with 0 errors.oasdiff breaking(v1.16.0, base=main): additive findings only (new oneOf member + new shared-enum value); no removals.mainmerge, re-ranmake buildand confirmed the committedopenapi.yaml/mintlify/openapi.yamlare byte-identical to a fresh bundle (no drift).Original PR: #732