Skip to content

Move Alfredpay to Alfred's Penny adapter - #1384

Draft
ebma wants to merge 25 commits into
stagingfrom
feat/alfredpay-penny-adapter
Draft

ebma wants to merge 25 commits into
stagingfrom
feat/alfredpay-penny-adapter

Conversation

@ebma

@ebma ebma commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Why

Alfredpay moved to a new platform. The legacy Penny hosts are gone (the production host answers an nginx 503 on every path, the dev/staging sandboxes no longer resolve), so no Alfredpay call can succeed against them. Alfred serves the same Penny routes through an adapter on the new platform that authenticates with a partner API key.

What

  • Client (AlfredpayApiService): Authorization: Bearer alfk_… on every JSON request and multipart upload; the api-key/api-secret pair and ALFREDPAY_API_SECRET are gone. ALFREDPAY_BASE_URL now defaults to the adapter for the environment: https://api.sandbox.alfredpay.io/adapters/penny when SANDBOX_ENABLED, else https://api.alfredpay.io/adapters/penny. The request paths stay the same.
  • Quotes: metadata no longer carries businessId. Alfred derives the company from the API key, and the migration guide says not to send one. AlfredpayQuoteMetadata is now closed to { customerId }, so it can't sneak back in.
  • GET /alfredpayStatus: an upstream 404 no longer demotes an approved customer back into onboarding. During the migration a lookup can 404 for data Alfred hasn't moved yet, and the new platform doesn't serve US. Customers who aren't approved still reset, as before.
  • Contract suite: a key alone is enough for the live half. Fixed-input quotes must now echo the input amount and move in the plausible direction. That catches a switch to minor units: Alfred's native API uses them, and the decimal regex alone can't tell "500" from "50000".
  • Limits: the adapter's allConfigs returns null minQuantity/maxQuantity on most pairs, meaning no limit on that side. The limits indexer turned that into new Big(null), which threw and aborted every refresh. The schema now accepts null, and a null bound keeps our configured bound instead of reading as unlimited.
  • Onramp order response: the adapter returns the created order flat, with fiatPaymentInstructions beside it, where Penny nested it under transaction. The mint lifecycle reads order.transaction.transactionId, so every onramp would have failed after creation. The client now returns the nested shape for both.
  • KYC forms: Alfred accepts only a CURP with a valid check digit as the Mexican dni (it rejected an INE number and a CURP with a wrong digit, and accepted a correct CURP that didn't match the customer's name), and it requires a CUIT for Argentine individuals. The shared KYC schema now validates both, so users get a field error instead of a failed submission.
  • Review fixes (vortex-review run on this PR, every finding verified before fixing):
    • Offramp: a failed read of the provider order before the transfer (404, 401, 5xx, timeout) failed the ramp with the user's funds on the ephemeral. It is now retried like the other provider reads; nothing has left the ephemeral at that point. Pre-existing, but the adapter makes these reads routine.
    • createOnramp validates the normalized order, so a response without a transactionId fails inside the financial operation instead of showing payment instructions for an order we can't track. A 409 limit body with a null maximum maps to the minimum breach. Uploads get the 30s timeout the JSON calls have.
    • Limits refresh: one bad row, an empty listing, an unknown customer type or a row on the wrong decimal scale no longer aborts or wipes the refresh; bound-less rows can't shadow real ones.
    • POST /v1/alfredpay/kyc (partners submit without our forms) now returns a 400 for an invalid CURP or a missing/invalid CUIT instead of the provider's opaque 422 → 500. The CUIT check digit is validated too (the sandbox enforces it). OpenAPI and the corridor guide document both.
    • GET /getKycStatus no longer resets an approved customer when the provider reports no submission, matching /alfredpayStatus.
    • Onramp simulation rejects a quote whose fromAmount doesn't echo the requested input (unit-switch guard, as the offramp side already has).
    • The live contract half runs only when ALFREDPAY_BASE_URL points at the sandbox, since the default is now production. Order tests use USDT like production.
    • The Mexican KYC field is labelled "CURP" in en and pt-BR.
  • Migration guards (decided 2026-10-01):
    • Status reads no longer move an approved customer to UPDATE_REQUIRED. The adapter reports it for customers Alfred lists as active, and /retryKyc only reopens FAILED, so they would have been locked out. Unapproved customers still move.
    • Offramp registration requires the fiatAccountId to be in the customer's account list and answers 400 otherwise, before an order exists. Bank accounts did not survive the migration, and ownership was otherwise checked only by the provider. Verified on the sandbox that the listing is per customer; the nightly now asserts it.
    • MX/CO business verification is paused until the KYB form is reworked. The shared KYC machine (widget and dashboard) stops it with a clear message before any provider call, createBusinessCustomer answers 503 for MX/CO, and onboarding discovery returns 404 for those combinations. The KYB flow tests lift the pause so the form stays covered; the dashboard's full KYB e2e is skipped until the rework.
  • Security spec: platform note, invariants 32–39, checklist entries.

Schema check (what could be verified without credentials)

  • Routes: an unauthenticated probe on production and sandbox found every route the client calls. A 401 means the route exists; made-up control routes still return 404. The one gap is POST …/kyc/{submissionId}/retry, which only the US individual retry path uses.
  • Requests vs Penny's published OpenAPI (the spec behind alfredpay.readme.io): the fields and enums we send match. That covers onramp/offramp/quote fields, fiat account type (SPEI, COELSA, ACH, BANK_USA) and accountType (CLABE, CBU/CVU/ALIAS, CORRIENTE/AHORRO, CHECKING/SAVING). Their docs omit a few fields Penny accepted in production (KYB questionnaire, KYC phoneNumber, MX document types). The live KYC/KYB flows are the check for those.
  • Responses: no Penny spec documents response bodies, so they can only be verified live. The consumed-response schemas in schemas.ts are the ones the nightly already validated against Penny's sandbox.

Live verification (2026-09-30)

  • Sandbox contract suite, with re-provisioned fixtures: 18 pass, 0 fail.
    • New AR, CO and MX customers reached COMPLETED through the real KYC calls. Uploads, submission and status responses all match our schemas.
    • MX bank accounts: create, list and delete work.
    • Onramp: order creation works (after the fix above), and reading the order back works.
  • Inconclusive: AR/CO bank account creation and offramp order creation. The sandbox answered 502 "Account validation service unavailable" and, as of 2026-10-01 16:29 UTC, 502 112003 "Service temporarily unavailable", so offramps are not verified yet. An hourly probe re-runs the live suite when it recovers.
  • KYB: the adapter's company requirement set changed. businessActivities/walletAddresses must be arrays, and 17 fields/documents our form doesn't collect are now required (business type, description, phone and incorporation date, an operating address, representative title/signer/control flags, terms acceptance, proof of business activity). New business onboarding needs a form rework, which is out of scope here. Existing business customers aren't migrated yet anyway.
  • Production, read-only, through this PR's client:
    • All 21 migrated individual customers resolve under their old IDs, and their bank account lists (empty) and KYC responses match our schemas.
    • All 5 business customers answer "Business Customer not found".

Before merging

  • Offramp order creation verified on the sandbox once Alfred's account validation service is back.
  • UPDATE_REQUIRED: the adapter reports it for all five migrated customers that /v1/customers lists as ACTIVE, and COMPLETED for the one listed as NOT_STARTED. Our sync would downgrade them. Waiting on Alfred.
  • Nightly secrets set (sandbox adapter URL and key, fixture IDs e6b69e2c…, 6f1032c5…, 7b3d0856…); CONTRACT_ALFREDPAY_API_SECRET deleted.
  • UPDATE_REQUIRED guard, MX/CO business pause, payout-account check (see Migration guards).

Deploy

  • Render: the shared "Vortex API" env group feeds prod and staging. Put the production alfk_ key in ALFREDPAY_API_KEY there, remove ALFREDPAY_API_SECRET and any ALFREDPAY_BASE_URL pointing at the legacy host. On the staging service, override ALFREDPAY_API_KEY (sandbox key) and ALFREDPAY_BASE_URL=https://api.sandbox.alfredpay.io/adapters/penny unless staging runs with SANDBOX_ENABLED.
  • Keep USD in DISABLED_FIAT_CURRENCIES (US unsupported on the new platform). Take MXN/COP/ARS out only after a read-only production smoke test.
  • Fiat accounts: the legacy API is gone, so we can't re-submit them ourselves. Confirm with Alfred that they migrated them on their side with the same IDs.

Tests

  • alfredpayApiService.test.ts: JSON requests and all three uploads send the bearer header and no legacy headers (fails on the old client).
  • alfredpay-status-not-found.integration.test.ts: approved customer survives an upstream 404 (fails without the guard); unapproved customer still resets.
  • mxn-offramp.scenario.test.ts: a 404/503/network error reading the order before the transfer is retried and the ramp completes with one submission (fails on the old executor).
  • alfredpay-limits.service.test.ts: per-row robustness, empty listing keeps the cache, wrong-scale rows skipped (4 fail on the old indexer).
  • identifiers.test.ts, schemas.test.ts, validators.test.ts: CURP/CUIT vectors at the form and the API boundary.
  • alfredpayApiService.test.ts: malformed onramp orders rejected, 409 with null maximum.
  • alfredpay-status-not-found.integration.test.ts: UPDATE_REQUIRED keeps an approved customer approved on both status routes (fails without the guard).
  • alfredpay-offramp.registration.test.ts: an unlisted account is rejected before createOfframp; a failed listing aborts the preflight.
  • machine.test.ts, alfredpay-business-kyb-paused.integration.test.ts, onboarding-requirements.route.test.ts, dashboard onboarding-alfredpay-mxn-kyb.spec.ts: the MX/CO business pause (unit and API tests fail without it).
  • Gates: API 1948 pass / 55 skip / 0 fail, shared 190, kyc 90, dashboard 161, frontend 187, dashboard Alfredpay e2e 12 passed / 1 skipped, bun typecheck, bun lint:fix, wire-contract:check, docs:api:check, live sandbox suite 18/0 plus the new scoping check.

Alfredpay moved to a new platform and decommissioned the Penny hosts
(production answers 503, the sandboxes no longer resolve). Its adapter
keeps the Penny paths but authenticates with a partner API key sent as
a bearer token, so the api-key/api-secret pair and ALFREDPAY_API_SECRET
go away. The base URL now defaults to the adapter for the environment,
like the other providers, instead of the dead dev host.
Alfred derives the company from the API key, and its migration guide
says not to send a business id in the request body. Closing
AlfredpayQuoteMetadata to { customerId } makes the compiler reject one
if it comes back.
GET /alfredpayStatus sent any customer back to onboarding when
Alfredpay answered 404 for their submission. During the platform
migration a lookup can 404 for data Alfred has not moved yet, and the
new platform does not serve US, so a status read would have forced
approved customers through KYC again. Unapproved customers still reset.
Alfred's native API serializes amounts in minor units. The quote
contract's decimal regex accepts both "500" and "50000", so a unit
switch in the adapter would pass the live suite unnoticed. A fixed
input must now come back unchanged and the output must move the
plausible way against it.
@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit 9affe4d
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6abe90358cbd3e0008393a51
😎 Deploy Preview https://deploy-preview-1384--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vrtx-dashboard ready!

Name Link
🔨 Latest commit 9affe4d
🔍 Latest deploy log https://app.netlify.com/projects/vrtx-dashboard/deploys/6abe9035aab6f000086cf801
😎 Deploy Preview https://deploy-preview-1384--vrtx-dashboard.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit 9affe4d
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6abe9035c273e300089735b0
😎 Deploy Preview https://deploy-preview-1384--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

ebma added 20 commits September 30, 2026 10:26
The Penny adapter serves null minQuantity/maxQuantity on most pairs,
meaning no limit on that side. The limits indexer passed null to Big,
which threw and aborted every refresh, so provider bounds such as the
ARS onramp minimum were never applied. A null bound now keeps our
configured bound instead of reading as unlimited.
Alfred's Penny adapter answers POST .../onramp with the order fields
flat and fiatPaymentInstructions beside them, where Penny nested the
order under `transaction`. The mint lifecycle reads
order.transaction.transactionId, so every onramp order would have
failed right after creation. The client now returns the nested shape
for both, in case Alfred restores Penny's format.
The new platform accepts only a CURP with a valid check digit as the
Mexican dni and rejects Argentine individuals without a CUIT, both
with a bare 110002 Invalid field(s). Our forms invited an INE number
and marked CUIT optional, so those users would fail at submission.
Validating both in the shared schema turns that into a field error.
The pinned customers belonged to the decommissioned Penny sandbox. The
new AR/CO/MX customers were provisioned on api.sandbox.alfredpay.io.
The KYC flow now uses a valid CURP, and every placeholder upload gets
distinct bytes, because Alfred rejects a document identical to one
already on the submission.
The pre-transfer read in ensureLiveProviderOrder had no catch, so a
404, 401, 5xx or timeout became an unrecoverable phase error and the
ramp failed with the user's deposit and the subsidy on the ephemeral.
The adapter switch makes such reads routine (orders it cannot find, a
wrong key during rollout). Nothing has left the ephemeral at that
point, so the read is now retried like the other provider reads.
createOnramp now parses the normalized order like createOfframp does,
so a response without a transactionId fails inside the financial
operation instead of showing the user instructions for an order we
cannot track. A 409 limit body with a null maximum (the adapter's
"no limit") now maps to the minimum breach. The multipart uploads get
the same 30s timeout as the JSON calls.
The sandbox rejects an Argentine CUIT with a wrong check digit, like
the CURP one, and accepts separators. The CURP and CUIT checks move
to @vortexfi/shared so the API validator can use them too.
Integrators submit KYC without our forms, and the provider answers a
bad CURP or a missing or wrong CUIT with an opaque 422 that reaches
them as a 500. validateKycSubmission now returns a 400 for both, and
the OpenAPI schema and corridor guide document the rules.
One absent or empty bound, an unknown customer type or an empty
listing aborted or wiped the whole refresh, and a row without any
bound could shadow a real one. Rows now count only with a decimal
bound on the scale the quote path reads back.
GET /getKycStatus reset any customer to onboarding when the provider
returned no submission, the case /alfredpayStatus already guards.
A fixed-input quote echoes its input. If the adapter ever switched to
minor units like Alfred's native API, every onramp figure would be off
by that factor; the offramp side already checks this.
The live half creates customers, accounts and orders, and the base
URL now defaults to production outside sandbox deployments. Order
tests use USDT like production, and the onramp test pins the
paymentType partners receive.
Alfred's adapter reports UPDATE_REQUIRED for migrated customers its own
API lists as ACTIVE. The status routes stored it, which demoted approved
customers to started, and /retryKyc only reopens FAILED, so they could
neither ramp nor re-verify. The provider-customer view, which every
status write goes through, now ignores it for an approved customer, and
/getKycStatus reports the stored status instead of the raw mapping.
Registration passed the caller's fiatAccountId straight to createOfframp,
so only the provider checked that the account belongs to the customer,
and nothing verified Alfred's new platform still does. Payout accounts
also did not survive the migration, so a saved id now fails with a
generic provider error. The preflight now requires the id in the
customer's account list and answers 400 before an order exists. The
nightly asserts the listing is scoped per customer.
Alfred's new platform requires KYB fields our form and API do not
collect (business type, operating address, signer details and more), so
every MX/CO business submission fails at the provider. One predicate
marks the pause for discovery, the KYC machine and the API, and
discovery stops advertising those flows until the KYB rework.
Business users in the widget and dashboard filled the whole KYB form and
then failed at submission. The machine now fails a paused MX/CO business
flow up front with a clear message, both for business input and for an
individual who switches to business. The KYB tests lift the pause so the
form flow stays covered for its rework.
A partner that skips discovery would otherwise create a business
customer whose KYB submission can only fail at the provider. The check
runs after the duplicate-customer check so an existing customer still
gets its specific answer. The managed delegation test moves to the US
corridor, which still onboards businesses.
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