Skip to content

feat!: sync the SDK with blindpay-v2's public API and docs - #83

Merged
rafa-thayto merged 28 commits into
mainfrom
rafa-thayto/ai-determinism
Oct 27, 2026
Merged

rafa-thayto merged 28 commits into
mainfrom
rafa-thayto/ai-determinism

Conversation

@rafa-thayto

Copy link
Copy Markdown
Contributor

Summary

This brings the SDK in line with blindpay-v2's current public API and docs, and makes api-sync (the deterministic patcher) generate everything it can, so we hand-write less.

Merging produces a 6.0.0 Version Packages PR. Every breaking change is listed in the major changesets.

Coverage against blindpay-v2

  • Every x-sdk: true operation in the current public spec has an SDK method: --coverage reports 0 gaps and --check reports no drift. x-sdk: false routes (yield, card payments, simulate, /upload/extract) are excluded by sdkSurface() in api-sync.
  • New resources: payables, instances.rfi, customers.rfi, instances.onboarding, custodial wallet deposits, upload presign/analyze, bankAccounts.createFasterPayments.
  • Fields: the 68 additive fields recorded in .api-sync/unmodeled.json as "needs a reviewed PR" are now modeled by --apply. That covers:
    • payout and payin tracking sub-fields and tracking_documents
    • partner, transaction and billing fee amounts
    • customer additional_info and geolocation
    • instance RFI email settings
    • Transfer.external_id
  • Spec nullability and enums are enforced by sync:check. It now also sees null expressed through $ref, anyOf, oneOf and allOf.

Fixes from the docs sweep (spec-confirmed only)

  • List responses can be a plain array when no pagination param is passed: payouts, customers, payables (payins already was).
  • List filters: payouts, payins and transfers accept every filter the spec documents.
  • Types that didn't match the API: bankAccounts.get, VirtualAccount.us, GetFeesResponse, partner fees (phantom wallet fields removed), payable from/to, customer owners on create and update, light KYC customers, BusinessIndustry, and payout create inputs (wallet_id path).
  • { success: true } bodies are typed as SuccessResponse instead of void.
  • Upload calls default instance_id to the client's instance.
  • Docs-only claims (where the docs disagree with the spec) were not encoded. They're listed under "For humans" below.

New capabilities

  • Idempotency keys: every authenticated mutation takes options?: RequestOptions. For example, blindpay.payouts.createEvm(input, { idempotencyKey }) sends the Idempotency-Key header. The api-sync generator emits the same signature, and request-options.test.ts fails if a mutation doesn't forward it.
  • Error envelope: error now carries code, description, errors and trace_id next to message.
  • Typed webhooks: blindpay.constructWebhookEvent(...) verifies with Svix and returns a WebhookEvent union discriminated on webhook_event.
  • EUR stablecoins: WalletToken adds EURC and EURB to transfers and wallet balances. Payins report bre_b.

api-sync (the deterministic patcher)

  • Honors blindpay-v2's x-sdk flag.
  • Infers field types correctly for $ref/allOf, arrays and enums.
  • Inserts operation methods with parent path IDs.
  • Merges into an existing changeset when the same spec is applied twice.
  • Follows composed nullability.
  • Re-applying the current spec is a no-op (deterministic).

Test plan

  • bun run check-types, lint:check, test (281), sync:check, contract-check, build: all exit 0
  • bun scripts/api-sync/index.ts --apply --spec <current v2 public spec> applies 0 changes
  • Golden test: deleted methods are regenerated by --apply, type-check, and are idempotent
  • A human look at RFI: it's the first implementation across the BlindPay SDKs

For humans (docs-only, not encoded in the SDK)

  • The docs disagree with themselves about whether external Solana payouts use /payouts/evm or /payouts/solana.
  • offramp-wallets.md says there's no list endpoint, but the spec has one.
  • Docs vs spec mismatches:
    • payin quote id prefix: docs say pq_, spec says qu_
    • cover_fees on transfer quotes: required in the docs, optional in the spec
    • partner-fee list fields
    • the transfer quote billing_fee_amount
  • The webhook catalog in the docs omits payin.partnerFee and payout.partnerFee.
  • The public spec publishes only the merged CreateCustomerIn, not the per-kyc_type required fields.

The SDK's types were largely written by the old Claude-driven api-sync and
then maintained by hand, so they drifted from the OpenAPI spec (501 audit
findings). This brings them back in line and makes the drift a CI failure.

- Add `| null` to the 454 fields the spec marks nullable (313 response,
  141 input), via a codemod driven by the type audit's own findings.
- Let the deterministic patcher insert the missing enum members it was
  blocked from applying (BankDetailKey, LimitIncreaseRequestSupportingDocumentType,
  BusinessIndustry, EstimatedTimeOfArrival), and retype six fields that were a
  bare string where the API guarantees a union, including a new BankAccountStatus.
- sync:check now enforces nullability, and typegen emits `| null` for required
  fields too, so generated methods pass the gate.
- Drop the dead .claude-pr/CLAUDE.md agent prompt and point CLAUDE.md at the
  generator before hand-writing anything.

Deliberate divergences (approved_rfi, swift_payment_code, provider_name,
discriminator narrowings, payin-fx narrowing) are left as recorded.
Shells only: each factory returns {} until its methods land. Wiring them
into client.ts and index.ts up front keeps the resource work in separate
files.
Two generator defects found by running the patcher against the current
public spec:

- --apply flagged the request/response schemas of operations it was about
  to generate (e.g. CreatePlaidLinkIn, AcceptTosOut) as new schemas needing a
  human, so it refused to apply its own output. The new-schema and
  enum-coverage gates now judge against the map as the run will leave it
  (withPendingMapEntries), which persistNewMapEntries now reuses.
- singularize() turned the tos segment into to (CreatePassthroughToInput).
…atcher

Payout.name/type, Transfer and CreateTransferResponse tracking_partner_fee,
and CreateQuoteInput bank_account_id/request_amount are nullable in the
current public spec. TrackingStatus gets an enums[] mapping so new step values
(pending_refund_review) are inserted by the patcher instead of failing as
needs-human on every tracking object.
…esign and analyze

- instances.get() returns Instance (RetrieveInstanceOut) with InstanceType and InstanceOnboardingStep
- instances.onboarding: getBusinessDetails, getBusinessProfile, getOwnershipDocuments
- upload.presign (POST /v1/presign) and upload.analyze (multipart POST /v1/upload/analyze)
- POST /v1/upload/extract intentionally not implemented (excluded from every SDK per blindpay-v2 docs)
Adds getDeposits, getYield, getYieldHistory, getYieldTransactions, enableYield and disableYield to blindpay.wallets.custodial, plus yield_status on CustodialWallet.
…r preapproval, CNAE and supporting-document types

Adds virtualAccounts.simulateSpeiDeposit / simulateBrebDeposit (development instances only), models CustomerVirtualAccountPreapproval and CustomerCnaeCondition on the three KYC/KYB response types, adds LimitIncreaseSupportingDocument and supporting_documents to the limit-increase request/response types, and widens LimitIncreaseRequestSupportingDocumentType to the spec's 15 values.
Every blindpay-v2 route declares 'x-sdk': true|false (enforced by its
check-sdk-tags.yml; apps/api/AGENTS.md: true includes it in the SDKs).
loadSpec now drops x-sdk:false operations, so they and the schemas only they
reach are never coverage gaps, new schemas or generator input.

This excludes the yield operations, the simulated SPEI/Bre-B deposits, card
payments, ToS passthrough and upload/extract. Drop the yield and simulate
methods built before the flag was found; keep custodial wallet deposits,
yield_status and the customer types, which x-sdk:true operations return.
…ing number

Adds instances.rfi and customers.rfi (get/submit) and swift_intermediary_bank_routing_number on international SWIFT bank account types.
- Generated methods dropped parent path IDs (customers/{customer_id}/...), so
  createPlaid referenced an undeclared customer_id. All path IDs after the
  instance are now passed, and a lone scalar is named after its own param.
- Field inserts typed enum properties as a bare string, which the type check
  then rejected, and object properties as Record<string, unknown>. They now
  reuse the matching shared union (importing it), emit a literal union, and
  compile object properties with the method generator's own typegen.
- insertEnumMember failed on a single-line union with no trailing semicolon
  (valid in types/index.d.ts).
- detectRemovals only reports enum members a mapped SDK union still declares,
  so a removal already made by hand does not block --apply.
Spec: blindpay-v2 origin/main apps/api/openapi.json through its own public
filter (the last api-sync delivery was August). Snapshot refreshed by the
patcher's --apply.

- Map the hand-built resources (payables, RFI, onboarding, custodial wallet
  deposits, upload presign/analyze, customer preapproval/CNAE/supporting
  documents) and ignore the new *WebhookOut payloads like the existing ones.
- Patcher output (575 changes): 283 enum members (BusinessIndustry's full
  NAICS list, Network, BankDetailKey, WebhookEvents, approved_rfi,
  pending_refund_review, ...), 291 fields (UK Faster Payments on bank
  accounts, Colombia/Mexico virtual account details, payable_id, ...) and the
  generated createPlaid method, plus its own changeset.
- Remove the ssb banking partner and the stale WalletInboundSchemaOut ignore
  entry; CreateQuoteInput now only requires network and token, as the spec does.
- Record the new inline nested shapes, with how each one is typed.
- api-sync field inserts now type a property that references a component
  ($ref, or an allOf that only annotates one): the SDK symbol it is mapped to
  in the same file, else the compiled component. EURB/EURC on custodial
  wallet balances were typed string; they are WalletTokenBalance.
- Encode instance_id in the upload, analyze and presign query strings.
- Fold WalletDepositNetwork into the shared Network union it now equals.
- sdkSurface drops path items left with no operations; flatten the
  field-insert import guard; put detectRemovals' doc back on it.
- Changeset: describe nullability without counts that drift; keep the ssb
  removal in the breaking list.
Names the inline forced_provider union PayoutForcedProvider and maps it in
spec-map.json so the patcher keeps it in sync; --apply against the current
public spec then added infinitus_portage.
WalletToken (USDC/USDT/USDB/EURC/EURB) replaces the deposits-only union and
types transfer tokens and wallet balances; StablecoinToken stays the
payin/payout set. PayinPaymentMethod is now anchored on PayinOut, the wider
set, and the patcher added bre_b; payin quotes keep PaymentMethod.

api-sync: a second --apply against the same spec merges into its changeset
instead of overwriting the first run's entries.
The API returns every match as a bare array unless a pagination param is
passed (anyOf in the spec, confirmed by the pagination docs). payouts,
customers and payables now model both branches, like payins.

api-sync: propsOf reads the object branches of an anyOf/oneOf body, so the
payables list mapping is compared against its envelope instead of nothing.
Drops the 68 additive gaps unmodeled.json held for a reviewed PR and lets
--apply insert them: payout and payin tracking sub-fields and
tracking_documents, partner/transaction/billing fee amounts, customer
additional_info and geolocation, instance RFI email settings, Transfer
external_id, bank-account IFSC/force_cpf_cnpj, fee tiers, requiredWhen and
more. Discriminators, provider-internal handles and the error envelope stay
recorded.

api-sync: an array field insert compiles its items (reusing a same-file
symbol for a referenced schema) instead of emitting unknown[]. New named
unions AdditionalInfoItem/AdditionalInfoLabel and PayoutProviderIntegration
are mapped so the patcher keeps them in sync; payin tracking provider_name
stays a string like its payout counterpart.
error now carries code, description, errors and trace_id next to message,
for JSON requests and multipart uploads alike (toErrorResponse).
InternalApiClient's post/put/patch/delete take an optional RequestOptions
whose idempotencyKey becomes the Idempotency-Key header; delete drops its
never-used body. The operation generator gives every mutation the same
trailing options parameter and forwards it, so generated and hand-written
methods keep one signature.
specTypeSignature read null only from a property's own type, so a nullable
$ref (PayableParty), an anyOf null branch (last_event_at) or v2's nullable
allOf wrapper (GetFeesOut.ted) passed sync:check silently. It now follows
those, and the 13 SDK fields that still dropped null carry it.
Every authenticated POST/PUT/PATCH/DELETE method takes a trailing
options?: RequestOptions and forwards it, so options.idempotencyKey is sent
as the Idempotency-Key header (the unauthenticated /e/ ToS route is left
out: the API ignores it there). request-options.test.ts fails on a mutation
that doesn't forward it; CLAUDE.md templates and the README show the
pattern.
constructWebhookEvent and verifyWebhookSignature share verifyWithSvix and
the exported WebhookVerificationInput type instead of two copies of the
same call and parameter shape.
- api-sync: alternatives() stops at a $ref cycle; a DELETE with a body is
  left to a human (the client's delete sends none); mergedBump naming.
- error: keep only the envelope's documented, well-typed fields.
- Idempotency-Key: an empty key is sent (the API rejects it) instead of
  silently dropped.
- offramp_wallets[] on bank accounts matches the API (network union,
  nullable external_id, circle_wallet_id).
- WebhookVerificationInput sits above the client class; CLAUDE.md structure,
  README webhook example and changesets updated.
@BernardoSM

BernardoSM commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues
✅ Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: 34d5fb6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@blindpay/node Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@rafa-thayto
rafa-thayto merged commit 22b6789 into main Oct 27, 2026
13 checks passed
@rafa-thayto
rafa-thayto deleted the rafa-thayto/ai-determinism branch October 27, 2026 22:34
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.

2 participants