payments: TransFi adapter, with setup you can actually automate - #1012
Merged
Conversation
Adds @profullstack/sh1pt-payment-transfi — cross-border payouts into local banks and wallets in ~100 countries, funded from stablecoin or fiat. A sending rail like worldremit, not a buyer-facing checkout. The point of it is the setup flow. WorldRemit issues credentials only after a conversation with their business team, so `sh1pt config payments` can do no more than print instructions. TransFi onboarding is self-serve: sign up, clear KYB, and displai.transfi.com issues sandbox and production credential pairs. That is automatable end to end, so this adapter opens the signup page, walks the operator through it, prompts for both halves of the pair, and stores them in the vault. connect() then verifies them for real. It calls GET /v3/balance and reports a 401 as bad credentials with a note that sandbox and production keys are separate, rather than only asserting a secret exists. Credentials that are present but wrong are indistinguishable from correct ones until something tries to move money, which is late. Authentication is HTTP Basic over `username:password`, verified from TransFi's published docs and pinned by a test. Worth stating plainly because a sibling implementation of this same API in coinpayportal sends `Authorization: Bearer` and would have failed every request the moment a real credential was set — the resulting 401 looks exactly like an expired key, so the mistake survives a long time. That one needs fixing separately. payout() validates its arguments and then refuses, because the request shape is unverified. Returning a fabricated transfer id would report money as sent when nothing left the account, which is worse than an error. It gets implemented against a sandbox payout, not against documentation. packages/social/ugig/README.md moves too: it is generated output that had drifted from its source label, and running the repo's own generator to create this package's README regenerates it. Note for anyone running these tests: packages/core must be built first. Otherwise resolution falls through to a stray copy under ~/node_modules and every payments adapter test fails with a confusing "Cannot find package 'vitest'". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcjnyR2gpRBzWYGobUvuHT
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
ThreatCrush Security Scan49 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 14 | LOW: 34
Snippets are redacted; ThreatCrush never prints matched credential material. |
The webhook secret is the one credential TransFi hands over before KYB clears, so it is the part of this integration that can be built and tested now. Wires it up end to end. Signature verification is HMAC-SHA256 over the raw request body, hex, compared against X-Transfi-Hmac-Hash — verified from TransFi's published docs. Two deliberate departures from their documented sample. It compares with timingSafeEqual rather than `===`, because a byte-by-byte string compare leaks how much of a forged signature was correct. And the header constant is lowercase: their sample reads req.headers['X-Transfi-Hmac-Hash'], which is always undefined in Node, since Node lowercases incoming header names. Payout events are mapped for both the fiat and crypto vocabularies, which name the same three moments differently. fund_scheduled is pending rather than succeeded — the order is booked and the recipient has nothing yet. An unrecognised status becomes pending, never a confident answer: TransFi can add statuses without asking us, and both confident answers cause harm, one reporting a payout as delivered and the other telling someone their money bounced when it did not. The normalized amount is the withdraw side, what left TransFi toward the recipient. TransFi's sample payloads label deposit and withdraw in a way that is easy to read backwards, so the raw payload is preserved intact for anything that depends on the distinction. Setup now captures TRANSFI_WEBHOOK_SECRET too, and the steps say plainly that it is available before KYB — which is what makes this worth doing first. 29 tests on this adapter, 94 across packages/payments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcjnyR2gpRBzWYGobUvuHT
ralyodio
marked this pull request as ready for review
September 6, 2026 14:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
@profullstack/sh1pt-payment-transfi— cross-border payouts into local banks and wallets in ~100 countries, funded from stablecoin or fiat. A sending rail like worldremit, not a buyer-facing checkout.Why this one is worth automating
WorldRemit issues credentials only after a conversation with their business team, so
sh1pt config paymentscan do no more than print instructions and hope.TransFi onboarding is self-serve: sign up, clear KYB, and
displai.transfi.comissues sandbox and production credential pairs. That is automatable end to end — so this adapter opens the signup page, walks the operator through the steps, prompts for both halves of the credential pair, and stores them in the vault.connect() is a real check, not a presence check
It calls
GET /v3/balanceand surfaces a 401 as bad credentials, with a note that sandbox and production pairs are separate. Credentials that are present but wrong are indistinguishable from correct ones right up until something tries to move money, which is far too late to find out.A bug this surfaced elsewhere
TransFi authenticates with HTTP Basic over
username:password— verified from their published docs and pinned by a test here.The sibling implementation of this same API in coinpayportal sends
Authorization: Bearerand would have failed every request the moment a real credential was configured. The resulting 401 looks exactly like an expired key, so it is the kind of mistake that survives a long time. That needs fixing separately in coinpayportal; this adapter has a test asserting the header is Basic and explicitly not Bearer, so this one cannot drift the same way.payout() refuses rather than pretends
It validates its arguments and then throws, because the request shape is unverified. Returning a fabricated transfer id would report money as sent when nothing left the account — worse than an error. It gets implemented against a real sandbox payout, not against documentation.
Testing
10 new tests; all 75 tests across
packages/paymentspass; the CLI typechecks.Two notes for reviewers:
packages/coremust be built before running these tests. Otherwise module resolution falls through to a stray copy under~/node_modulesand every payments adapter test fails with a confusingCannot find package 'vitest'. This is pre-existing — worldremit's suite fails identically on a clean worktree — but it cost me a detour, so it is worth knowing.packages/social/ugig/README.mdmoves in this diff. It is generated output that had drifted from its source label ("Prompts Marketplace" → "AI Gig Marketplace"), and running the repo's ownscripts/gen-module-readmes.mjsto create this package's README regenerates it. I kept the generator's output rather than deliberately re-staling it.🤖 Generated with Claude Code
https://claude.ai/code/session_01KcjnyR2gpRBzWYGobUvuHT