From 0e4dab8ee46ba78965f1cc963e705e327304cf13 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 10 Jul 2026 11:27:58 +0200 Subject: [PATCH 1/8] docs(api): add SEO metadata, update AI-agent page and skill for all corridors - Record per-page Apidog SEO settings (metaTitle, metaDescription, keywords) in page-manifest.json and document the convention in the docs README; values are applied manually in the Apidog UI. - De-BRL the AI Agent Integration page: state that all live corridors (BRL, EUR, USD, MXN, COP, ARS) are supported on every integration path and rewrite the fiat-settlement section per corridor. - Bring the vortex-integration skill up to date with the SDK: EUR/SEPA and USD/MXN/COP/ARS recipes, taxId deprecation, submitUserTransactions and listAlfredpayFiatAccounts, new error classes, and fix the sandbox base URL (api-sandbox.vortexfinance.co). --- .agents/skills/vortex-integration/SKILL.md | 223 ++++++++++++++++++--- docs/api/README.md | 12 ++ docs/api/apidog/page-manifest.json | 147 ++++++++++++++ docs/api/pages/12-ai-agent-integration.md | 13 +- 4 files changed, 362 insertions(+), 33 deletions(-) diff --git a/.agents/skills/vortex-integration/SKILL.md b/.agents/skills/vortex-integration/SKILL.md index cb71f976e..99eee4bcc 100644 --- a/.agents/skills/vortex-integration/SKILL.md +++ b/.agents/skills/vortex-integration/SKILL.md @@ -1,6 +1,6 @@ --- name: vortex-integration -description: Use when integrating Vortex or @vortexfi/sdk, including quotes, BRL PIX onramps/offramps, ramp register/update/start/status flows, webhook verification, ephemeral key custody, supported corridors, sandbox/production auth, and recovery from ramp errors. +description: Use when integrating Vortex or @vortexfi/sdk, including quotes, onramps/offramps for BRL (PIX), EUR (SEPA), USD (ACH), MXN (SPEI), COP, and ARS (CBU), ramp register/update/start/status flows, webhook verification, ephemeral key custody, supported corridors, sandbox/production auth, and recovery from ramp errors. --- # Vortex Integration Skill @@ -14,14 +14,17 @@ A machine-loadable capability catalog for AI coding agents integrating Vortex in ## Global Context (read once) - **SDK**: `@vortexfi/sdk` (JavaScript/TypeScript). Install: `npm i @vortexfi/sdk`. -- **API base URLs**: production `https://api.vortexfinance.co`, sandbox `https://api.sandbox.vortexfinance.co`. +- **API base URLs**: production `https://api.vortexfinance.co`, sandbox `https://api-sandbox.vortexfinance.co`. - **Auth keys**: partner integrations use a key pair. - `pk_live_*` / `pk_test_*` — public key, sent in request bodies for partner attribution. - `sk_live_*` / `sk_test_*` — secret key, sent in the `X-API-Key` header. **Never expose `sk_*` in a browser or mobile app.** + - For USD/MXN/COP/ARS ramps the `sk_*` key must be the **user's own user-linked key** — registration resolves the user's KYC and payment profile from the authenticated account. - **Decimals**: all amounts are strings. Never parse them through JS `Number` — use `BigInt`, `decimal.js`, or equivalent. - **Quote TTL**: quotes expire (see `expiresAt`). Re-quote, never reuse stale quotes. - **Ramp counts**: ephemeral keys sign exactly 5 presigned transactions per ramp. The API rejects anything else. -- **Currently implemented corridors**: BRL (PIX) onramp and offramp. EUR (SEPA) types exist in the SDK but the handlers throw `"Euro onramp handler not implemented yet"` / `"Euro offramp handler not implemented yet"` at runtime. Treat EUR as `status: planned`. +- **Currently implemented corridors** (all live in the SDK): BRL via PIX, EUR via SEPA (Mykobo), USD via ACH, MXN via SPEI, COP via ACH, ARS via CBU. All support both onramp (BUY) and offramp (SELL). EUR and the bank-transfer corridors deliver to EVM networks only (no AssetHub). +- **EUR enum value**: EUR quotes use `FiatToken.EURC` (not `EUR`) as the currency value, with `"sepa"` as the rail identifier. +- **taxId is deprecated for BRL**: the user's tax ID is derived server-side from the user-linked `sk_*` key. Sending a `taxId` that mismatches the derived one is rejected; stop sending it in new integrations. - **No secret in markdown**: never paste API keys into source files, logs, screenshots, or support tickets. --- @@ -100,7 +103,6 @@ For the best price across all networks, use `POST /v1/quotes/best` (same body, o ## Common failures - `MissingRequiredFieldsError` — missing input/output/amount/network. - `InvalidNetworkError` — `network` not in the supported list (see `discover-supported-corridors`). -- `400` with `EuroOnrampHandlerNotImplemented` — EUR corridors are planned, not active. - Quote returned but unused for > TTL → `QuoteExpiredError` on subsequent `registerRamp`. Re-quote. --- @@ -119,18 +121,17 @@ triggers: ``` ## When to use -The user is in Brazil (or has BRL/PIX access) and wants to buy crypto. KYC must be completed beforehand through the Vortex app or Widget — `taxId` (CPF/CNPJ) is the required link. +The user is in Brazil (or has BRL/PIX access) and wants to buy crypto. KYC must be completed beforehand through the Vortex app or Widget; the user's CPF/CNPJ is resolved server-side from their user-linked `sk_*` key. ## Prerequisites - Fresh quote with `rampType: BUY`, `from: "pix"`, `inputCurrency: FiatToken.BRL`. - `destinationAddress` — the user's wallet on the target network. -- `taxId` — the user's CPF or CNPJ; must match a KYC'd Vortex subaccount. +- The user has completed BRL KYC; `taxId` is **deprecated** — it is derived from the authenticated key, and a mismatching value is rejected. ## SDK recipe ```js const { rampProcess } = await vortex.registerRamp(quote, { - destinationAddress: "0xUserWalletAddress", - taxId: "12345678900" + destinationAddress: "0xUserWalletAddress" }); // Show user the PIX payment instructions @@ -159,8 +160,7 @@ curl -X POST https://api.vortexfinance.co/v1/ramp/register \ { "type": "Stellar", "address": "G..." } ], "additionalData": { - "destinationAddress": "0xUserWalletAddress", - "taxId": "12345678900" + "destinationAddress": "0xUserWalletAddress" }, "publicKey": "'"$VORTEX_PUBLIC_KEY"'" }' @@ -178,10 +178,10 @@ curl -X POST https://api.vortexfinance.co/v1/ramp/start \ If you implement this without the SDK, follow the raw-API contract in [`AI_AGENT_INTEGRATION` § D.3–D.5](https://api-docs.vortexfinance.co/ai-agent-integration). ## Common failures -- `SubaccountNotFoundError` — the `taxId` has no KYC'd Vortex subaccount. Direct the user to KYC first. +- `SubaccountNotFoundError` — the tax ID derived from the authenticated key has no KYC'd Vortex subaccount. Direct the user to KYC first. - `KycInvalidError` — KYC exists but is not approved. - `AmountExceedsLimitError` — quote amount above the user's KYC tier limit. -- `MissingBrlParametersError` — `destinationAddress` or `taxId` missing. +- `MissingBrlParametersError` — `destinationAddress` missing. - `QuoteExpiredError` — re-quote and call `registerRamp` again. - `TimeWindowExceededError` on `startRamp` — too long elapsed since `registerRamp`; restart the flow. @@ -206,25 +206,26 @@ The user holds crypto on an EVM chain and wants to receive BRL via PIX. Unlike o ## Prerequisites - Fresh quote with `rampType: SELL`, `to: "pix"`, `outputCurrency: FiatToken.BRL`. - `pixDestination` — recipient's PIX key (validate via `GET /v1/brla/validatePixKey` if uncertain). -- `receiverTaxId` — CPF/CNPJ of the PIX recipient. -- `taxId` — the user's KYC'd CPF/CNPJ. - `walletAddress` — the user's source wallet address. +- Optional: `receiverTaxId` — CPF/CNPJ of the PIX recipient when paying out to someone other than the user. `taxId` is **deprecated** (derived from the authenticated key). ## SDK recipe ```js const { rampProcess, unsignedTransactions } = await vortex.registerRamp(quote, { pixDestination: "user@example.com", - receiverTaxId: "12345678900", - taxId: "12345678900", walletAddress: "0xUserWalletAddress" }); -// Identify which txs the END USER must sign (vs. ephemerals, which SDK signed already) -const userTxs = await vortex.getUserTransactions(rampProcess, "0xUserWalletAddress"); - -// userTxs typically includes: SquidRouter approve, SquidRouter swap, AssetHub→Pendulum XCM -// Have the user sign each one and submit on-chain. Collect the resulting tx hashes. +// Easiest path: let the SDK classify, sign, broadcast, and submit the user-owned txs +// via your wallet callbacks (e.g. @wagmi/core): +await vortex.submitUserTransactions(rampProcess.id, unsignedTransactions, { + signTypedData: payload => signTypedData(wagmiConfig, payload), + sendTransaction: tx => sendTransaction(wagmiConfig, tx) +}); +// Lower-level alternative: filter the user txs yourself, have the user sign and +// broadcast them, then push the hashes back: +const userTxs = await vortex.getUserTransactions(rampProcess, "0xUserWalletAddress"); await vortex.updateRamp(quote, rampProcess.id, { squidRouterApproveHash: "0xapprove...", squidRouterSwapHash: "0xswap...", @@ -239,7 +240,7 @@ await vortex.startRamp(rampProcess.id); Same three-step pattern: `POST /v1/ramp/register` → user signs → `POST /v1/ramp/update` with the collected hashes → `POST /v1/ramp/start`. See [`AI_AGENT_INTEGRATION` § D.3–D.5](https://api-docs.vortexfinance.co/ai-agent-integration) for the exact body shapes. ## Common failures -- `MissingBrlOfframpParametersError` — `receiverTaxId`, `pixDestination`, or `taxId` missing. +- `MissingBrlOfframpParametersError` — `pixDestination` or `walletAddress` missing. - `InvalidPixKeyError` — PIX key format invalid or unreachable. Validate beforehand with `GET /v1/brla/validatePixKey`. - `InvalidPresignedTxsError` on `updateRamp` — hash format wrong, or the on-chain tx does not match the unsigned tx that was issued. Re-sign exactly what `getUserTransactions` returned. - `NoPresignedTransactionsError` on `startRamp` — `updateRamp` was not called or did not include the required hashes. @@ -247,6 +248,164 @@ Same three-step pattern: `POST /v1/ramp/register` → user signs → `POST /v1/r --- +```yaml +--- +name: start-ramp-eur-sepa +description: Initiate a EUR onramp or offramp via SEPA. Onramp shows IBAN transfer instructions; offramp requires user-signed on-chain transactions. +triggers: + - "EUR onramp" + - "EUR offramp" + - "SEPA" + - "buy crypto with euro" + - "sell crypto for euro" + - "IBAN payment" +--- +``` + +## When to use +The user has a SEPA bank account and wants to buy or sell crypto for EUR. EUR onboarding is individual KYC only and requires a connected wallet; complete it through the Vortex app or Widget first. EUR delivers to EVM networks only (no AssetHub). + +## Prerequisites +- Quote with `inputCurrency: FiatToken.EURC` and `from: "sepa"` (buy), or `outputCurrency: FiatToken.EURC` and `to: "sepa"` (sell). Note: the enum value is `EURC`, not `EUR`. +- `destinationAddress`, `email`, `ipAddress` — required for both directions. +- `walletAddress` — additionally required for offramp (the user's source wallet). + +## SDK recipe (onramp) +```js +const quote = await vortex.createQuote({ + rampType: RampDirection.BUY, + from: EPaymentMethod.SEPA, + to: Networks.Base, + network: Networks.Base, + inputAmount: "100", + inputCurrency: FiatToken.EURC, + outputCurrency: EvmToken.USDC +}); + +const { rampProcess } = await vortex.registerRamp(quote, { + destinationAddress: "0xUserWalletAddress", + email: "user@example.com", + ipAddress: "203.0.113.1" +}); + +// SEPA transfer instructions are on the ramp after registration: +console.log(rampProcess.ibanPaymentData.iban); +console.log(rampProcess.ibanPaymentData.receiverName); +console.log(rampProcess.ibanPaymentData.reference); + +// After the user completed the SEPA transfer: +await vortex.startRamp(rampProcess.id); +``` + +No user-signed on-chain transactions are required for EUR onramps. + +## SDK recipe (offramp) +```js +const { rampProcess, unsignedTransactions } = await vortex.registerRamp(quote, { + destinationAddress: "0xUserWalletAddress", + email: "user@example.com", + ipAddress: "203.0.113.1", + walletAddress: "0xUserWalletAddress" +}); + +// User signs and broadcasts squidRouterApprove + squidRouterSwap, then: +await vortex.updateRamp(quote, rampProcess.id, { + squidRouterApproveHash: "0xapprove...", + squidRouterSwapHash: "0xswap..." +}); +await vortex.startRamp(rampProcess.id); +``` + +`submitUserTransactions` (see `start-offramp-brl`) works here as well. + +## Common failures +- `MissingMykoboOnrampParametersError` / `MissingMykoboOfframpParametersError` — `destinationAddress`, `email`, `ipAddress`, or `walletAddress` missing. +- `MykoboKycRequiredError` — the user has not completed EUR KYC; onboard via the Vortex app or Widget. +- EUR can be gated off server-side per environment; a rejected EUR quote does not necessarily mean a client bug. + +--- + +```yaml +--- +name: start-ramp-bank-transfer +description: Initiate an onramp or offramp for USD (ACH), MXN (SPEI), COP (ACH), or ARS (CBU) through Vortex's local payment partners. +triggers: + - "USD onramp" + - "MXN onramp" + - "COP offramp" + - "ARS ramp" + - "SPEI" + - "ACH" + - "CBU" + - "bank transfer ramp" +--- +``` + +## When to use +The user wants to ramp USD, MXN, COP, or ARS over their domestic banking rail. These corridors **require a user-linked `sk_*` key**: registration resolves the user's KYC and payment profile from the authenticated account. Partner-scoped keys cannot register ramps here. EVM networks only (no AssetHub). + +| Fiat | Rail identifier | Payment rail | +|------|-----------------|--------------| +| `USD` | `"ach"` | ACH bank transfer | +| `MXN` | `"spei"` | SPEI transfer | +| `COP` | `"ach"` | Colombian bank transfer | +| `ARS` | `"cbu"` | CBU bank transfer | + +## Prerequisites +- The user completed KYC for the corridor's country via the Vortex app or Widget, and the SDK is authenticated with that user's own `sk_*` key. +- Buy: `destinationAddress` (required); `fiatAccountId`, `walletAddress` optional. +- Sell: `fiatAccountId` and `walletAddress` (both required). List saved accounts with `vortex.listAlfredpayFiatAccounts(country)`. + +## SDK recipe (onramp, MXN shown — substitute fiat + rail for USD/COP/ARS) +```js +const quote = await vortex.createQuote({ + rampType: RampDirection.BUY, + from: EPaymentMethod.SPEI, + to: Networks.Polygon, + network: Networks.Polygon, + inputAmount: "201", + inputCurrency: FiatToken.MXN, + outputCurrency: EvmToken.USDC +}); + +const { rampProcess } = await vortex.registerRamp(quote, { + destinationAddress: "0xUserWalletAddress" +}); + +const started = await vortex.startRamp(rampProcess.id); + +// Bank transfer instructions the user must pay are on the START response: +console.log(started.achPaymentData); +``` + +No user-signed on-chain transactions on buys. Unlike BRL there is no QR code — display the `achPaymentData` deposit instructions verbatim; the ramp continues automatically once the fiat deposit is confirmed. + +## SDK recipe (offramp) +```js +const accounts = await vortex.listAlfredpayFiatAccounts("MEX"); + +const { rampProcess, unsignedTransactions } = await vortex.registerRamp(quote, { + fiatAccountId: accounts[0].id, + walletAddress: "0xUserWalletAddress" +}); + +await vortex.submitUserTransactions(rampProcess.id, unsignedTransactions, { + signTypedData: payload => signTypedData(wagmiConfig, payload), + sendTransaction: tx => sendTransaction(wagmiConfig, tx) +}); +await vortex.startRamp(rampProcess.id); +``` + +The SDK cannot **create** fiat accounts; they are created during onboarding in the Vortex app or Widget. `fiatAccountId` is opaque to the SDK. + +## Common failures +- `MissingAlfredpayOnrampParametersError` / `MissingAlfredpayOfframpParametersError` — `destinationAddress`, `fiatAccountId`, or `walletAddress` missing. +- `AlfredpayOnrampKycRequiredError` — the authenticated user has no approved KYC for the corridor's country. +- `401` on register — the `sk_*` key is partner-scoped, not user-linked. Mint a user key after email OTP sign-in. +- `InsufficientBalanceError` — the offramp pre-flight found the source wallet balance below the quote's input amount. + +--- + ```yaml --- name: poll-ramp-status @@ -322,7 +481,8 @@ First-time integration, environment migration, or when an agent needs to decide | Key | Where it goes | Purpose | |-----|---------------|---------| | `pk_live_*` / `pk_test_*` | Anywhere (browser-safe) | Partner attribution. Sent inside request bodies as `publicKey`. | -| `sk_live_*` / `sk_test_*` | Server-side only | API auth. Sent as `X-API-Key` header. **Never** ship to browser/mobile bundles. | +| `sk_live_*` / `sk_test_*` (partner-scoped) | Server-side only | API auth for BRL/EUR ramps and webhook management. Sent as `X-API-Key` header. **Never** ship to browser/mobile bundles. | +| `sk_live_*` / `sk_test_*` (user-linked) | Server-side only | Required for USD/MXN/COP/ARS ramps; also derives the BRL `taxId`. Minted programmatically after email OTP sign-in; shown once at creation. | ## SDK recipe ```js @@ -492,7 +652,7 @@ try { const quote = await vortex.createQuote({ /* candidate combination */ }); // → corridor is live } catch (err) { - if (err.name === "InvalidNetworkError" || err.message.includes("not implemented")) { + if (err.name === "InvalidNetworkError" || err.name === "MissingRequiredFieldsError") { // → corridor not supported } else { throw err; @@ -500,11 +660,11 @@ try { } ``` -## Current corridor reality (May 2026) -- **BRL via PIX**: onramp and offramp both live. -- **EUR via SEPA**: SDK types exist (`EurOnrampQuote`, `EurOfframpQuote`) but handlers throw `"Euro onramp/offramp handler not implemented yet"` at runtime. Treat as `planned`. -- **ARS via CBU**: supported via the AlfredPay corridor; route resolver determines availability per-combination. -- **USD / MXN / COP via ACH / SPEI / WIRE**: supported via the AlfredPay corridor; route resolver determines availability per-combination. +## Current corridor reality (July 2026) +- **BRL via PIX**: onramp and offramp both live. `taxId` deprecated — derived from the user-linked key. +- **EUR via SEPA (Mykobo)**: onramp and offramp both live in the SDK (`FiatToken.EURC`, rail `"sepa"`). May be gated off server-side per environment; verify with a quote. +- **USD (ACH) / MXN (SPEI) / COP (ACH) / ARS (CBU)**: onramp and offramp live via the AlfredPay corridor; requires a user-linked `sk_*` key. Route resolver determines availability per-combination. +- All corridors deliver to EVM networks; AssetHub is only available for BRL routes. ## Common failures - Filtering by a fiat that has no payment methods → empty array, not an error. @@ -551,8 +711,11 @@ Include this payload (with secrets redacted) in any support ticket. | `QuoteNotFoundError` | Wrong env or stale id | Verify base URL; re-quote | | `InvalidNetworkError` | Network not in `Networks` enum | Use `discover-supported-corridors` | | `MissingRequiredFieldsError` / `MissingBrlParametersError` / `MissingBrlOfframpParametersError` | Body field missing | Fill the missing field; do not retry blindly | -| `SubaccountNotFoundError` / `KycInvalidError` | KYC issue | Direct user through KYC; do not retry programmatically | +| `SubaccountNotFoundError` / `KycInvalidError` | BRL KYC issue | Direct user through KYC; do not retry programmatically | +| `MykoboKycRequiredError` / `AlfredpayOnrampKycRequiredError` | EUR / bank-transfer-corridor KYC issue | Onboard the user via the Vortex app or Widget; do not retry programmatically | | `AmountExceedsLimitError` | Above KYC tier | Lower amount or upgrade KYC | +| `InsufficientBalanceError` | Offramp pre-flight: source wallet balance below the quoted input | Top up the wallet or lower the amount, then re-register from a fresh quote | +| `EphemeralNotFreshError` / `EphemeralFreshnessCheckError` | Generated ephemeral account was not fresh, or freshness could not be verified | Safe to retry `registerRamp` — the SDK generates new ephemerals each attempt | | `InvalidPixKeyError` | Bad recipient PIX key | Validate via `GET /v1/brla/validatePixKey`, then re-register | | `InvalidPresignedTxsError` | Submitted signed tx does not match the issued unsigned tx (chainId, nonce, gas, recipient, or value mismatch) | Re-sign exactly what `getUserTransactions` returned; do not reuse old signatures | | `NoPresignedTransactionsError` | `startRamp` called before `updateRamp` | Submit the required hashes via `updateRamp` first | diff --git a/docs/api/README.md b/docs/api/README.md index 7fbd896f8..69dc8366b 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -54,6 +54,18 @@ bun run docs:api:export The current slug-to-source mapping is the `slug` field in `apidog/page-manifest.json`. Keep them aligned with the published Apidog pages. +## SEO Settings + +Apidog supports per-page SEO settings (Page → SEO Settings): URL Slug, Meta Title, Meta Description, Keywords, and Custom Metadata. There is no API for setting them, so they are applied manually in the Apidog UI. + +The intended values are recorded in the `seo` block of each page entry in `apidog/page-manifest.json` (`metaTitle`, `metaDescription`, `keywords`). That block is the repository source of truth: when page content changes in a way that affects its summary, update the manifest `seo` block in the same change, then re-apply it in Apidog when publishing. + +Conventions: + +- Meta titles ≤ 60 characters, leading with what the page is about and ending with a Vortex identifier where space allows. +- Meta descriptions 140–160 characters, written as a plain-language answer to what a searcher would want from the page. +- Keywords target integrator search intent (e.g. "crypto on-ramp API", "PIX crypto", "SEPA crypto"), not internal terminology. + ## Publishing To Apidog Apidog's documented Git connection currently targets OpenAPI/Swagger files. Use it for `docs/api/openapi/vortex.openapi.json`. diff --git a/docs/api/apidog/page-manifest.json b/docs/api/apidog/page-manifest.json index 67bd7e51b..3cf055ccd 100644 --- a/docs/api/apidog/page-manifest.json +++ b/docs/api/apidog/page-manifest.json @@ -51,78 +51,225 @@ "pages": [ { "order": 1, + "seo": { + "keywords": [ + "crypto on-ramp API", + "crypto off-ramp API", + "fiat to crypto API", + "stablecoin payments API", + "cross-border payments", + "USDC", + "USDT", + "Vortex API" + ], + "metaDescription": "Vortex is a fiat-to-crypto on/off-ramp API supporting BRL, EUR, USD, MXN, COP, and ARS with USDC and USDT payouts across major EVM networks and AssetHub.", + "metaTitle": "Vortex API Overview — Fiat-to-Crypto On/Off-Ramp Gateway" + }, "slug": "overview", "source": "docs/api/pages/01-overview.md", "title": "Overview" }, { "order": 2, + "seo": { + "keywords": [ + "Vortex SDK", + "@vortexfi/sdk", + "crypto on-ramp SDK", + "Node.js on-ramp integration", + "PIX crypto API", + "SPEI crypto API", + "buy USDC API" + ], + "metaDescription": "Run your first fiat-to-crypto ramp with the Vortex Node.js SDK: install @vortexfi/sdk, create a quote, register the ramp, sign transactions, and track it to completion.", + "metaTitle": "Quick Start — Vortex Node.js SDK (@vortexfi/sdk)" + }, "slug": "quick-start-with-the-sdk", "source": "docs/api/pages/02-quick-start-with-the-sdk.md", "title": "Quick Start With The SDK" }, { "order": 3, + "seo": { + "keywords": [ + "Vortex API keys", + "API authentication", + "secret key", + "public key", + "partner key", + "OTP sign-in", + "crypto ramp authentication" + ], + "metaDescription": "How Vortex authenticates API clients: pk_*/sk_* key pairs, partner-scoped vs user-linked keys, email OTP sign-in, and minting user API keys programmatically.", + "metaTitle": "Authentication And API Keys — Vortex API" + }, "slug": "authentication-and-partner-keys", "source": "docs/api/pages/03-authentication-and-partner-keys.md", "title": "Authentication And API Keys" }, { "order": 4, + "seo": { + "keywords": [ + "ramp lifecycle", + "crypto ramp flow", + "quote register start", + "unsigned transactions", + "ramp status", + "on-ramp process" + ], + "metaDescription": "The full Vortex ramp lifecycle: create a quote, register with ephemeral accounts, sign and update transactions, settle fiat, start the ramp, and track it to a terminal state.", + "metaTitle": "Ramp Lifecycle — Quote, Register, Sign, Start, Track" + }, "slug": "ramp-lifecycle", "source": "docs/api/pages/04-ramp-lifecycle.md", "title": "Ramp Lifecycle" }, { "order": 5, + "seo": { + "keywords": [ + "ephemeral keys", + "key custody", + "non-custodial ramp", + "crypto key management", + "ramp recovery", + "secure key storage" + ], + "metaDescription": "Vortex never receives ephemeral secret keys. Learn what your integration must store, for how long, and how to recover in-flight ramps safely.", + "metaTitle": "Ephemeral Key Custody — Vortex Security Model" + }, "slug": "ephemeral-key-custody", "source": "docs/api/pages/05-ephemeral-key-custody.md", "title": "Ephemeral Key Custody" }, { "order": 6, + "seo": { + "keywords": [ + "crypto quote API", + "exchange rate API", + "on-ramp pricing", + "off-ramp fees", + "best route quote", + "stablecoin conversion rates" + ], + "metaDescription": "Create and read Vortex quotes: request shapes for buys and sells, fee breakdowns, best-route selection, expiry handling, and partner pricing.", + "metaTitle": "Quotes And Pricing — Vortex API" + }, "slug": "quotes-and-pricing", "source": "docs/api/pages/06-quotes-and-pricing.md", "title": "Quotes And Pricing" }, { "order": 7, + "seo": { + "keywords": [ + "webhooks", + "webhook signature verification", + "RSA-PSS", + "ramp status events", + "payment notifications", + "crypto payment webhooks" + ], + "metaDescription": "Subscribe to Vortex ramp lifecycle events, verify RSA-PSS webhook signatures, and handle retries and auto-deactivation correctly.", + "metaTitle": "Webhooks — Real-Time Ramp Events And Verification" + }, "slug": "webhooks", "source": "docs/api/pages/07-webhooks.md", "title": "Webhooks" }, { "order": 8, + "seo": { + "keywords": [ + "crypto widget", + "hosted on-ramp checkout", + "embed crypto on-ramp", + "fiat to crypto widget", + "buy crypto widget", + "white-label ramp" + ], + "metaDescription": "Embed the hosted Vortex Widget for browser and mobile flows: session creation, quote handoff, theming, and the custody model it handles for you.", + "metaTitle": "Vortex Widget — Hosted Crypto On/Off-Ramp Checkout" + }, "slug": "widget-integration", "source": "docs/api/pages/08-widget-integration.md", "title": "Widget Integration" }, { "order": 9, + "seo": { + "keywords": [ + "PIX crypto", + "SEPA crypto", + "ACH crypto", + "SPEI crypto", + "CBU crypto", + "BRL on-ramp", + "EUR off-ramp", + "Brazil Mexico Colombia Argentina crypto" + ], + "metaDescription": "Corridor requirements for BRL (PIX), EUR (SEPA), USD (ACH), MXN (SPEI), COP, and ARS (CBU): payment rails, KYC prerequisites, fiat accounts, and limits.", + "metaTitle": "Fiat Corridors — PIX, SEPA, ACH, SPEI, CBU" + }, "slug": "fiat-corridors", "source": "docs/api/pages/09-fiat-corridors.md", "title": "Fiat Corridors" }, { "order": 10, + "seo": { + "keywords": ["sandbox", "test environment", "crypto API testing", "test API keys", "mock KYC accounts"], + "metaDescription": "Test Vortex integrations end-to-end in the sandbox: base URLs, pk_test_/sk_test_ keys, and pre-configured KYC-approved mock accounts.", + "metaTitle": "Sandbox — Test Your Vortex Integration" + }, "slug": "sandbox", "source": "docs/api/pages/10-sandbox.md", "title": "Sandbox" }, { "order": 11, + "seo": { + "keywords": [ + "production checklist", + "go live", + "crypto API security", + "key management checklist", + "payment integration launch" + ], + "metaDescription": "Everything to verify before taking a Vortex integration live: key handling, ephemeral custody, webhook verification, idempotency, and recovery runbooks.", + "metaTitle": "Production Checklist — Go Live With Vortex" + }, "slug": "production-checklist", "source": "docs/api/pages/11-production-checklist.md", "title": "Production Checklist" }, { "order": 12, + "seo": { + "keywords": [ + "AI agent integration", + "coding agent API docs", + "LLM integration guide", + "reimplement SDK", + "crypto ramp API contract", + "machine-readable API docs" + ], + "metaDescription": "Build a production-quality Vortex client in any language — with or without an AI coding agent. Covers the full raw-API contract, signing rules, and mandatory client responsibilities.", + "metaTitle": "AI Agent Integration — Build A Vortex Client In Any Language" + }, "slug": "ai-agent-integration", "source": "docs/api/pages/12-ai-agent-integration.md", "title": "AI Agent Integration" }, { "order": 13, + "seo": { + "keywords": ["KYB", "business verification", "know your business", "KYB deep link", "business onboarding crypto"], + "metaDescription": "Send business users straight into KYB verification with a single Vortex Widget URL — no quote required. Flow, query parameters, and locking behavior.", + "metaTitle": "KYB Deep Link — Business Verification Without A Quote" + }, "slug": "kyb-deep-link", "source": "docs/api/pages/13-kyb-deep-link.md", "title": "KYB Deep Link" diff --git a/docs/api/pages/12-ai-agent-integration.md b/docs/api/pages/12-ai-agent-integration.md index 1866dd93a..2c8831700 100644 --- a/docs/api/pages/12-ai-agent-integration.md +++ b/docs/api/pages/12-ai-agent-integration.md @@ -22,6 +22,8 @@ When you point an AI coding agent at Vortex: | Browser, mobile, WebView | Use the [Vortex Widget](https://api-docs.vortexfinance.co/widget-integration). | | Anything else (Go, Rust, Elixir, Java, Ruby, PHP, .NET, Deno, edge runtimes, …) | Reimplement the SDK behavior against the raw API as described in Section D below. | +Every path supports all live fiat corridors: BRL (PIX), EUR (SEPA), USD (ACH), MXN (SPEI), COP, and ARS (CBU). The corridor determines the register-time fields and the fiat settlement step, not the integration shape — see [Fiat Corridors](https://api-docs.vortexfinance.co/fiat-corridors) for per-corridor requirements. + Do not call the raw ramp API from a browser. Browsers cannot safely hold `sk_*` keys or ephemeral secrets. Use the Widget or proxy through a trusted backend. ## C. Python (`vortex-sdk-python`) @@ -133,14 +135,19 @@ Body includes the `rampId`, the transaction reference, and either the signed pay ### D.5 Fiat Payment And Start -- **BRL buy**: the register response contains `depositQrCode` (PIX). Show it; wait for the user to pay. Then call `POST /v1/ramp/start`. -- **BRL sell**: the user signs the user-owned transaction(s) and you submit them via update. Then call start. Vortex pays out to the user's PIX key. - ``` POST /v1/ramp/start X-API-Key: sk_* ``` +On a **buy**, where the fiat payment instructions appear depends on the corridor: + +- **BRL**: the register response contains `depositQrCode` (PIX). Show it; wait for the user to pay; then call start. +- **EUR**: the register response contains `ibanPaymentData` (IBAN, receiver name, payment reference). Show it; the user completes the SEPA transfer; then call start. +- **USD, MXN, COP, ARS**: call start first; the start response's `achPaymentData` contains the bank transfer instructions for the corridor's rail (ACH, SPEI, CBU). Display them verbatim; the ramp continues automatically once the deposit is confirmed. + +On a **sell**, the flow is the same in every corridor: the user signs the user-owned transaction(s), you submit them via update, then call start. Vortex pays out on the corridor's rail — the user's PIX key (BRL), SEPA account (EUR), or the saved bank account referenced by `fiatAccountId` (USD, MXN, COP, ARS). + ### D.6 Track - Register a webhook via `POST /v1/webhook` against `quoteId` or `sessionId`. Verify every delivery using RSA-PSS / SHA-256 against `GET /v1/public-key`. See [Webhooks](https://api-docs.vortexfinance.co/webhooks). From f5126962d6468eb8b93cbc918f0a1950d3800ff1 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 10 Jul 2026 11:46:33 +0200 Subject: [PATCH 2/8] docs(api): correct auth model, quote shape, and payment-instruction timing Verified against the API backend (staging and main): - Ramp registration requires a user-linked sk_* key in every corridor; partner keys without a user binding are rejected. Fix the auth page, corridors page, and quick start, which still claimed partner-scoped keys suffice for BRL/EUR with taxId identifying the user. - BRL taxId is an optional cross-check against the account's derived tax ID; drop it from the quick-start examples. - Quote response uses flattened fee fields (networkFeeFiat, totalFeeFiat, feeCurrency, *Usd twins), not a nested fee object; rewrite the sample on the quotes page. - depositQrCode/ibanPaymentData are released after presigned txs validate (update response / GET ramp), not on register; document on the lifecycle and AI-agent pages. - Generalize remaining BRL-only wording on the overview and lifecycle pages; whitelabel provider names on the KYB page. - Skill: EUR register gate returns 503 while quotes still succeed; webhook transactionType values are BUY/SELL; partner-scoped keys cannot register ramps. --- .agents/skills/vortex-integration/SKILL.md | 14 +++++----- docs/api/pages/01-overview.md | 2 +- docs/api/pages/02-quick-start-with-the-sdk.md | 9 +++--- .../03-authentication-and-partner-keys.md | 9 +++--- docs/api/pages/04-ramp-lifecycle.md | 6 ++-- docs/api/pages/06-quotes-and-pricing.md | 28 ++++++++++++------- docs/api/pages/09-fiat-corridors.md | 4 +-- docs/api/pages/12-ai-agent-integration.md | 4 +-- docs/api/pages/13-kyb-deep-link.md | 4 +-- 9 files changed, 44 insertions(+), 36 deletions(-) diff --git a/.agents/skills/vortex-integration/SKILL.md b/.agents/skills/vortex-integration/SKILL.md index 99eee4bcc..555a6105b 100644 --- a/.agents/skills/vortex-integration/SKILL.md +++ b/.agents/skills/vortex-integration/SKILL.md @@ -18,7 +18,7 @@ A machine-loadable capability catalog for AI coding agents integrating Vortex in - **Auth keys**: partner integrations use a key pair. - `pk_live_*` / `pk_test_*` — public key, sent in request bodies for partner attribution. - `sk_live_*` / `sk_test_*` — secret key, sent in the `X-API-Key` header. **Never expose `sk_*` in a browser or mobile app.** - - For USD/MXN/COP/ARS ramps the `sk_*` key must be the **user's own user-linked key** — registration resolves the user's KYC and payment profile from the authenticated account. + - **Ramp registration requires a user-linked `sk_*` key in every corridor** — the register call is rejected unless the authenticated key resolves to a user account. KYC identity (BRL tax ID, Alfredpay customer, Mykobo customer) is derived from that account, never from request fields. - **Decimals**: all amounts are strings. Never parse them through JS `Number` — use `BigInt`, `decimal.js`, or equivalent. - **Quote TTL**: quotes expire (see `expiresAt`). Re-quote, never reuse stale quotes. - **Ramp counts**: ephemeral keys sign exactly 5 presigned transactions per ramp. The API rejects anything else. @@ -321,7 +321,7 @@ await vortex.startRamp(rampProcess.id); ## Common failures - `MissingMykoboOnrampParametersError` / `MissingMykoboOfframpParametersError` — `destinationAddress`, `email`, `ipAddress`, or `walletAddress` missing. - `MykoboKycRequiredError` — the user has not completed EUR KYC; onboard via the Vortex app or Widget. -- EUR can be gated off server-side per environment; a rejected EUR quote does not necessarily mean a client bug. +- `503` "EUR ramps are currently disabled" on **register** — EUR is feature-gated server-side. EURC quotes still succeed while the gate is on, so a successful quote does not prove the corridor is enabled; probe registration in sandbox before shipping. --- @@ -401,7 +401,7 @@ The SDK cannot **create** fiat accounts; they are created during onboarding in t ## Common failures - `MissingAlfredpayOnrampParametersError` / `MissingAlfredpayOfframpParametersError` — `destinationAddress`, `fiatAccountId`, or `walletAddress` missing. - `AlfredpayOnrampKycRequiredError` — the authenticated user has no approved KYC for the corridor's country. -- `401` on register — the `sk_*` key is partner-scoped, not user-linked. Mint a user key after email OTP sign-in. +- `400` "requires an API key linked to a user" on register — the `sk_*` key is partner-scoped, not user-linked. Mint a user key after email OTP sign-in. - `InsufficientBalanceError` — the offramp pre-flight found the source wallet balance below the quote's input amount. --- @@ -481,8 +481,8 @@ First-time integration, environment migration, or when an agent needs to decide | Key | Where it goes | Purpose | |-----|---------------|---------| | `pk_live_*` / `pk_test_*` | Anywhere (browser-safe) | Partner attribution. Sent inside request bodies as `publicKey`. | -| `sk_live_*` / `sk_test_*` (partner-scoped) | Server-side only | API auth for BRL/EUR ramps and webhook management. Sent as `X-API-Key` header. **Never** ship to browser/mobile bundles. | -| `sk_live_*` / `sk_test_*` (user-linked) | Server-side only | Required for USD/MXN/COP/ARS ramps; also derives the BRL `taxId`. Minted programmatically after email OTP sign-in; shown once at creation. | +| `sk_live_*` / `sk_test_*` (partner-scoped) | Server-side only | Webhook management and partner attribution. Sent as `X-API-Key` header. **Cannot register ramps** unless the key is also linked to a user. **Never** ship to browser/mobile bundles. | +| `sk_live_*` / `sk_test_*` (user-linked) | Server-side only | Required for ramp registration in every corridor; corridor identity (BRL taxId, Alfredpay/Mykobo customer) is derived from the linked account. Minted programmatically after email OTP sign-in; shown once at creation. | ## SDK recipe ```js @@ -594,7 +594,7 @@ export async function verifyVortexWebhook(req, apiBaseUrl) { "sessionId": "...", "transactionId": "...", "transactionStatus": "PENDING | COMPLETE | FAILED", - "transactionType": "onramp | offramp" + "transactionType": "BUY | SELL" } } ``` @@ -662,7 +662,7 @@ try { ## Current corridor reality (July 2026) - **BRL via PIX**: onramp and offramp both live. `taxId` deprecated — derived from the user-linked key. -- **EUR via SEPA (Mykobo)**: onramp and offramp both live in the SDK (`FiatToken.EURC`, rail `"sepa"`). May be gated off server-side per environment; verify with a quote. +- **EUR via SEPA (Mykobo)**: onramp and offramp fully implemented in the SDK (`FiatToken.EURC`, rail `"sepa"`), but registration is feature-gated server-side and currently returns `503` "EUR ramps are currently disabled" when the gate is on. Quotes succeed regardless — probe registration, not quoting. - **USD (ACH) / MXN (SPEI) / COP (ACH) / ARS (CBU)**: onramp and offramp live via the AlfredPay corridor; requires a user-linked `sk_*` key. Route resolver determines availability per-combination. - All corridors deliver to EVM networks; AssetHub is only available for BRL routes. diff --git a/docs/api/pages/01-overview.md b/docs/api/pages/01-overview.md index 5609f8070..d859ecbd9 100644 --- a/docs/api/pages/01-overview.md +++ b/docs/api/pages/01-overview.md @@ -17,7 +17,7 @@ Every Vortex ramp follows the same shape: 1. **Quote** — your application requests pricing for a route. 2. **Register** — your application creates per-chain ephemeral accounts and submits their public addresses with the quote ID. Vortex returns one or more **unsigned** transactions that move funds through the ramp. 3. **Sign and update** — your application signs each unsigned transaction with the correct key (ephemeral key for SDK-controlled accounts, user wallet for the user's funds) and submits the signed payloads back to Vortex. -4. **Settle fiat** — for BRL buys, the user pays a PIX QR; for BRL sells, Vortex pays out to the user's PIX key after settlement. +4. **Settle fiat** — on buys, the user pays on the corridor's rail (a PIX QR for BRL, a SEPA transfer for EUR, bank transfer instructions for USD/MXN/COP/ARS); on sells, Vortex pays out on that rail after settlement. 5. **Start** — your application calls start once signatures and fiat payment are in place. 6. **Track** — Vortex drives the on-chain phase machine. Your application listens via webhooks or polls the ramp status endpoint. diff --git a/docs/api/pages/02-quick-start-with-the-sdk.md b/docs/api/pages/02-quick-start-with-the-sdk.md index 23578ab16..efe648b96 100644 --- a/docs/api/pages/02-quick-start-with-the-sdk.md +++ b/docs/api/pages/02-quick-start-with-the-sdk.md @@ -49,8 +49,7 @@ const quote = await sdk.createQuote({ }); const { rampProcess } = await sdk.registerRamp(quote, { - destinationAddress: "0x1234567890123456789012345678901234567890", - taxId: "12345678900" // user's CPF + destinationAddress: "0x1234567890123456789012345678901234567890" }); // Show the PIX QR to the user and wait for them to pay. @@ -60,7 +59,7 @@ console.log(rampProcess.depositQrCode); const started = await sdk.startRamp(rampProcess.id); ``` -The user must have completed BRLA KYC level 1 or higher under the same `taxId`. Partner `sk_*` keys cannot drive BRLA KYC; onboard the user through the Vortex app or Widget first. +The user must have completed BRL KYC level 1 or higher, and the SDK must be authenticated with that user's own user-linked `sk_*` key: the user's CPF/CNPJ is derived from the authenticated account. The `taxId` field is deprecated — if you still send it, it must match the tax ID on the account or registration is rejected. Partner keys cannot drive KYC and cannot register ramps; onboard the user through the Vortex app or Widget first. ## BRL Offramp (Sell) @@ -78,8 +77,6 @@ const quote = await sdk.createQuote({ const { rampProcess, unsignedTransactions } = await sdk.registerRamp(quote, { pixDestination: "user@example.com", - receiverTaxId: "12345678900", - taxId: "12345678900", walletAddress: "0xUSER..." }); @@ -87,6 +84,8 @@ const { rampProcess, unsignedTransactions } = await sdk.registerRamp(quote, { // user's behalf. Route them to the user's wallet (see below). ``` +The PIX payout goes to `pixDestination`, which must belong to the user. To pay out to a different recipient, pass `receiverTaxId` with the recipient's CPF/CNPJ; it defaults to the user's own tax ID. + ### Signing The User Transaction With Wagmi The user-owned transactions are EVM typed-data payloads or EVM transactions. Keep wallet prompts in your application and let the SDK handle classification and submission: diff --git a/docs/api/pages/03-authentication-and-partner-keys.md b/docs/api/pages/03-authentication-and-partner-keys.md index fb8e5fafa..fe0d410f5 100644 --- a/docs/api/pages/03-authentication-and-partner-keys.md +++ b/docs/api/pages/03-authentication-and-partner-keys.md @@ -7,8 +7,7 @@ Vortex authenticates API clients with public/secret key pairs, and accepts Supab | Task | Credential | |---|---| | Quote attribution and partner pricing | `pk_*` public key (optional on quotes) | -| BRL and EUR ramps from a partner backend | Partner-scoped or user-linked `sk_*` secret key | -| USD, MXN, COP, ARS ramps | **User-linked** `sk_*` secret key | +| Ramp registration, all corridors | **User-linked** `sk_*` secret key | | Webhook management | Partner secret key | | Minting and managing user API keys | `Authorization: Bearer` session token | @@ -24,14 +23,14 @@ Secret keys use the `sk_live_*` or `sk_test_*` prefix. They authenticate operati Secret keys come in two scopes: -- **Partner-scoped** keys are issued to a partner organization. They are sufficient for BRL and EUR ramps, where the user is identified by request fields such as the tax ID, and they are required for webhook management. -- **User-linked** keys are minted by a user's own Vortex account (see the *Provisioning User-Linked Keys* section below). Requests authenticated with them act as that user, so KYC completed by the same account applies automatically. The USD, MXN, COP, and ARS corridors require a user-linked key at ramp registration; see [Fiat Corridors](https://api-docs.vortexfinance.co/fiat-corridors). A user-linked key works in every corridor, so integrations can use it uniformly. +- **Partner-scoped** keys are issued to a partner organization. They authenticate webhook management and partner attribution. A partner key that is not linked to a user account cannot register ramps in any corridor. +- **User-linked** keys are minted by a user's own Vortex account (see the *Provisioning User-Linked Keys* section below). Requests authenticated with them act as that user, so KYC completed by the same account applies automatically. **Ramp registration requires a user identity in every corridor** — the ramp acts as the key's user, and corridor identity fields are derived from that account rather than taken from the request. For BRL, an explicitly provided `taxId` is accepted only as a cross-check: it must match the tax ID on the authenticated account. See [Fiat Corridors](https://api-docs.vortexfinance.co/fiat-corridors). Secret keys must be treated as server-side credentials. Do not expose them in browser bundles, mobile app binaries, URLs, screenshots, analytics tools, logs, or support tickets. When a request includes `partnerId`, the API may require the secret key to authenticate the matching partner. If the authenticated partner does not match the requested partner, Vortex rejects the request. -Ramp endpoints, including register, update, start, status, history, and error logs, require authentication through either a secret key or a Supabase Bearer token. +Ramp endpoints, including register, update, start, status, history, and error logs, require authentication through either a secret key or a Supabase Bearer token. Registration additionally requires the authenticated identity to resolve to a user — a secret key with no linked user is rejected with `400`. Webhook endpoints require a partner secret key and do not accept Supabase Bearer tokens. diff --git a/docs/api/pages/04-ramp-lifecycle.md b/docs/api/pages/04-ramp-lifecycle.md index 28bfae645..fbf554374 100644 --- a/docs/api/pages/04-ramp-lifecycle.md +++ b/docs/api/pages/04-ramp-lifecycle.md @@ -20,11 +20,13 @@ Only public addresses are sent to Vortex. The matching ephemeral secret keys mus Use `POST /v1/ramp/update` to submit signed transactions and route-specific transaction hashes. -The SDK performs this automatically for supported flows. On BRL buy flows, the SDK calls `POST /v1/ramp/update` inside `registerRamp` to submit presigned transactions. Direct API integrations must ensure that each signature or transaction hash matches the transaction returned by Vortex for the same ramp and phase. +The SDK performs this automatically for supported flows. On buy flows, the SDK calls `POST /v1/ramp/update` inside `registerRamp` to submit presigned transactions. Direct API integrations must ensure that each signature or transaction hash matches the transaction returned by Vortex for the same ramp and phase. + +On buys, the fiat payment instructions (`depositQrCode` for BRL, `ibanPaymentData` for EUR) are withheld until the presigned transactions pass validation: they are released on the update response and on `GET /v1/ramp/{id}`, not on the register response. SDK integrations receive them directly from `registerRamp`, which performs the update internally. ## 4. Start The Ramp -Use `POST /v1/ramp/start` after required signatures, transaction hashes, and fiat payment steps are complete. For BRL buy flows, call start after the user completes the PIX payment. +Use `POST /v1/ramp/start` after required signatures, transaction hashes, and fiat payment steps are complete. For BRL buys, call start after the user completes the PIX payment; for EUR buys, after the SEPA transfer. For USD, MXN, COP, and ARS buys the order is inverted: call start first — the start response's `achPaymentData` contains the bank transfer instructions the user must pay. ## 5. Track Status diff --git a/docs/api/pages/06-quotes-and-pricing.md b/docs/api/pages/06-quotes-and-pricing.md index 42584554b..9246f1ec3 100644 --- a/docs/api/pages/06-quotes-and-pricing.md +++ b/docs/api/pages/06-quotes-and-pricing.md @@ -46,20 +46,28 @@ Content-Type: application/json "inputCurrency": "BRL", "outputAmount": "27.41", "outputCurrency": "USDC", - "fee": { - "network": "0.42", - "anchor": "1.50", - "vortex": "0.75", - "partner": "0.00", - "total": "2.67", - "currency": "BRL" - }, - "expiresAt": "2025-05-18T12:35:00.000Z" + "network": "polygon", + "paymentMethod": "pix", + "networkFeeFiat": "0.42", + "anchorFeeFiat": "1.50", + "vortexFeeFiat": "0.75", + "partnerFeeFiat": "0.00", + "processingFeeFiat": "2.25", + "totalFeeFiat": "2.67", + "feeCurrency": "BRL", + "networkFeeUsd": "0.08", + "anchorFeeUsd": "0.28", + "vortexFeeUsd": "0.14", + "partnerFeeUsd": "0.00", + "processingFeeUsd": "0.42", + "totalFeeUsd": "0.50", + "expiresAt": "2026-07-10T12:35:00.000Z" } ``` - All monetary fields are decimal strings, not numbers; preserve them as strings end-to-end. -- `fee.currency` is the currency in which the fee fields are denominated. +- Fees are flattened into per-component fields, each provided twice: once denominated in `feeCurrency` (the `*Fiat` fields) and once in USD (the `*Usd` fields). `processingFee` is the sum of the anchor and Vortex components. +- When a quote-time discount applies, optional `discountFiat`, `discountUsd`, and `discountCurrency` fields are present. - `expiresAt` is short (typically a few minutes). Register the ramp promptly or request a new quote. ## Best-Quote Selection diff --git a/docs/api/pages/09-fiat-corridors.md b/docs/api/pages/09-fiat-corridors.md index 9b50270f4..0b77492c1 100644 --- a/docs/api/pages/09-fiat-corridors.md +++ b/docs/api/pages/09-fiat-corridors.md @@ -4,9 +4,9 @@ This page collects what each fiat corridor requires before a ramp can be registe ## BRL (PIX) -BRL routes settle over PIX and require user onboarding with Vortex's local payment partner before ramping. The user's Brazilian tax ID — CPF for individuals, CNPJ for businesses — is the primary identifier, so ramp registration may be authenticated with a partner-scoped `sk_*` key: the `taxId` in the request identifies the user. +BRL routes settle over PIX and require user onboarding with Vortex's local payment partner before ramping. The user's Brazilian tax ID — CPF for individuals, CNPJ for businesses — is the identity under which KYC is completed, but it is not how the ramp identifies the user: registration must be authenticated with the user's own **user-linked** `sk_*` key, and the tax ID is derived from that account. A `taxId` field may still be provided for backwards compatibility, but only as a cross-check — it must match the account's tax ID, and it cannot select a different user or claim an unlinked tax ID. -Level 1 onboarding collects basic identity information and enables lower-limit BRL flows. Level 2 adds document and liveness verification and may be required for higher limits or stricter compliance rules. The user must have completed KYC under the same `taxId` used in the ramp; otherwise the ramp may fail or require additional account-management steps. +Level 1 onboarding collects basic identity information and enables lower-limit BRL flows. Level 2 adds document and liveness verification and may be required for higher limits or stricter compliance rules. The user must have completed KYC on the same account whose key registers the ramp; otherwise the ramp may fail or require additional account-management steps. Partner integrations cannot drive BRL KYC with only `sk_*` or `pk_*` keys. The BRLA endpoints are first-party, user-oriented flows that rely on a Vortex-authenticated user context. When possible, use the Vortex application or hosted widget to complete onboarding before ramp execution. Business users can be sent straight into verification with the [KYB Deep Link](https://api-docs.vortexfinance.co/kyb-deep-link). diff --git a/docs/api/pages/12-ai-agent-integration.md b/docs/api/pages/12-ai-agent-integration.md index 2c8831700..1f5c3a8dd 100644 --- a/docs/api/pages/12-ai-agent-integration.md +++ b/docs/api/pages/12-ai-agent-integration.md @@ -142,8 +142,8 @@ X-API-Key: sk_* On a **buy**, where the fiat payment instructions appear depends on the corridor: -- **BRL**: the register response contains `depositQrCode` (PIX). Show it; wait for the user to pay; then call start. -- **EUR**: the register response contains `ibanPaymentData` (IBAN, receiver name, payment reference). Show it; the user completes the SEPA transfer; then call start. +- **BRL**: `depositQrCode` (PIX) is released once the presigned transactions submitted via update pass validation — on the update response and on `GET /v1/ramp/{id}`, not on the register response. Show it; wait for the user to pay; then call start. (The SDK performs the update inside `registerRamp`, so SDK callers see it on the returned ramp process.) +- **EUR**: `ibanPaymentData` (IBAN, receiver name, payment reference) follows the same release rule as `depositQrCode`. Show it; the user completes the SEPA transfer; then call start. - **USD, MXN, COP, ARS**: call start first; the start response's `achPaymentData` contains the bank transfer instructions for the corridor's rail (ACH, SPEI, CBU). Display them verbatim; the ramp continues automatically once the deposit is confirmed. On a **sell**, the flow is the same in every corridor: the user signs the user-owned transaction(s), you submit them via update, then call start. Vortex pays out on the corridor's rail — the user's PIX key (BRL), SEPA account (EUR), or the saved bank account referenced by `fiatAccountId` (USD, MXN, COP, ARS). diff --git a/docs/api/pages/13-kyb-deep-link.md b/docs/api/pages/13-kyb-deep-link.md index 373c0fd99..46f016422 100644 --- a/docs/api/pages/13-kyb-deep-link.md +++ b/docs/api/pages/13-kyb-deep-link.md @@ -10,7 +10,7 @@ It is a variant of the [hosted widget](https://api-docs.vortexfinance.co/widget- ?kyb / ?kybLocked → email + OTP sign-in → region selector → provider KYB → "KYB Completed" screen ``` -- **Brazil** routes to Avenia KYB. The user enters the company name and CNPJ together on the company form, then completes Avenia's hosted company and representative verification. +- **Brazil** routes to the local payment partner's KYB flow. The user enters the company name and CNPJ together on the company form, then completes the partner's hosted company and representative verification. - **Mexico / Colombia / USA** route to the local payment partner's business KYB form (the business customer type is preselected). - Europe is intentionally excluded — it is individual KYC only and requires a connected wallet, so it cannot complete a quote-less KYB deep link. @@ -62,6 +62,6 @@ window.open( ## Underlying KYB Onboarding -For Brazil, the deep link drives `POST /v1/brla/createSubaccount` **without** a `quoteId` — the subaccount is created from the company name and CNPJ collected on the form, and the optional quote association is simply omitted. See [Fiat Corridors](https://api-docs.vortexfinance.co/fiat-corridors) for how BRLA onboarding relates to the ramp flow. +For Brazil, the deep link drives `POST /v1/brla/createSubaccount` **without** a `quoteId` — the subaccount is created from the company name and CNPJ collected on the form, and the optional quote association is simply omitted. See [Fiat Corridors](https://api-docs.vortexfinance.co/fiat-corridors) for how BRL onboarding relates to the ramp flow. --- From 1eb4562d0a0efa735f3d4762a6ac4743715b6486 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 13 Jul 2026 09:35:24 +0200 Subject: [PATCH 3/8] docs(api): fix register request shape and BRL error messages per PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses copilot review on PR #1266, verified against the API backend: - Register body takes `signingAccounts` (array of {address, type} where type is "Substrate" or "EVM"), not `ephemeralAccounts`, and has no `publicKey` field — partner attribution rides on the quote's apiKey. Fix the skill REST example and the AI-agent D.3 contract. - The SDK generates exactly one Substrate + one EVM ephemeral (no Stellar); correct the ephemeral-custody page and D.3 accordingly. - BRL onramp/offramp missing-parameter failures now surface the real backend messages ("Parameter destinationAddress is required for onramp", "pixDestination is required for offramp to BRL", "User address must be provided for offramping."). Note that the SDK's MissingBrl*ParametersError classes map only the legacy messages and no longer fire for these. --- .agents/skills/vortex-integration/SKILL.md | 17 +++++++------- docs/api/pages/05-ephemeral-key-custody.md | 2 +- docs/api/pages/12-ai-agent-integration.md | 26 ++++++++++++++++------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/.agents/skills/vortex-integration/SKILL.md b/.agents/skills/vortex-integration/SKILL.md index 555a6105b..ce68fea36 100644 --- a/.agents/skills/vortex-integration/SKILL.md +++ b/.agents/skills/vortex-integration/SKILL.md @@ -148,21 +148,22 @@ The SDK generates ephemeral keypairs, signs internal txs, and submits them in `r ## REST fallback ```bash -# 1. Register +# 1. Register. signingAccounts holds the PUBLIC addresses of the ephemerals you +# generated for this ramp: one Substrate and one EVM account (the only two +# account types the API accepts). Partner attribution is carried by the quote's +# apiKey, so register takes no publicKey field. curl -X POST https://api.vortexfinance.co/v1/ramp/register \ -H "Content-Type: application/json" \ -H "X-API-Key: $VORTEX_SECRET_KEY" \ -d '{ "quoteId": "QUOTE_ID", - "ephemeralAccounts": [ - { "type": "EVM", "address": "0x..." }, + "signingAccounts": [ { "type": "Substrate", "address": "5..." }, - { "type": "Stellar", "address": "G..." } + { "type": "EVM", "address": "0x..." } ], "additionalData": { "destinationAddress": "0xUserWalletAddress" - }, - "publicKey": "'"$VORTEX_PUBLIC_KEY"'" + } }' # 2. Sign the returned `unsignedTxs` with the corresponding ephemeral keys (see AI_AGENT_INTEGRATION D.4) @@ -181,7 +182,7 @@ If you implement this without the SDK, follow the raw-API contract in [`AI_AGENT - `SubaccountNotFoundError` — the tax ID derived from the authenticated key has no KYC'd Vortex subaccount. Direct the user to KYC first. - `KycInvalidError` — KYC exists but is not approved. - `AmountExceedsLimitError` — quote amount above the user's KYC tier limit. -- `MissingBrlParametersError` — `destinationAddress` missing. +- Missing `destinationAddress` → `400 "Parameter destinationAddress is required for onramp"`. (The SDK's `MissingBrlParametersError` maps only the legacy `"...destinationAddress and taxId..."` message and will not fire for this; treat the raw 400 message as authoritative.) - `QuoteExpiredError` — re-quote and call `registerRamp` again. - `TimeWindowExceededError` on `startRamp` — too long elapsed since `registerRamp`; restart the flow. @@ -240,7 +241,7 @@ await vortex.startRamp(rampProcess.id); Same three-step pattern: `POST /v1/ramp/register` → user signs → `POST /v1/ramp/update` with the collected hashes → `POST /v1/ramp/start`. See [`AI_AGENT_INTEGRATION` § D.3–D.5](https://api-docs.vortexfinance.co/ai-agent-integration) for the exact body shapes. ## Common failures -- `MissingBrlOfframpParametersError` — `pixDestination` or `walletAddress` missing. +- Missing `pixDestination` → `400 "pixDestination is required for offramp to BRL"`. Missing `walletAddress` → `400 "User address must be provided for offramping."` (The SDK's `MissingBrlOfframpParametersError` maps only the legacy `"receiverTaxId, pixDestination and taxId..."` message and will not fire for these; treat the raw 400 message as authoritative.) - `InvalidPixKeyError` — PIX key format invalid or unreachable. Validate beforehand with `GET /v1/brla/validatePixKey`. - `InvalidPresignedTxsError` on `updateRamp` — hash format wrong, or the on-chain tx does not match the unsigned tx that was issued. Re-sign exactly what `getUserTransactions` returned. - `NoPresignedTransactionsError` on `startRamp` — `updateRamp` was not called or did not include the required hashes. diff --git a/docs/api/pages/05-ephemeral-key-custody.md b/docs/api/pages/05-ephemeral-key-custody.md index eb04c8bc9..8dde64061 100644 --- a/docs/api/pages/05-ephemeral-key-custody.md +++ b/docs/api/pages/05-ephemeral-key-custody.md @@ -1,6 +1,6 @@ # Ephemeral Key Custody -Ephemeral accounts are temporary blockchain accounts created for a single ramp. The SDK creates fresh chain-specific accounts for each flow, such as Stellar, Substrate, or EVM accounts depending on the route. They may hold funds in transit while Vortex coordinates swaps, transfers, bridge operations, or payment settlement. +Ephemeral accounts are temporary blockchain accounts created for a single ramp. The SDK creates a fresh Substrate (sr25519) account and a fresh EVM (secp256k1) account per ramp; one of each covers every leg of the route. They may hold funds in transit while Vortex coordinates swaps, transfers, bridge operations, or payment settlement. Vortex receives only ephemeral public addresses. Vortex does not receive, store, log, or reconstruct ephemeral secret keys. diff --git a/docs/api/pages/12-ai-agent-integration.md b/docs/api/pages/12-ai-agent-integration.md index 1f5c3a8dd..6a9c5ee2d 100644 --- a/docs/api/pages/12-ai-agent-integration.md +++ b/docs/api/pages/12-ai-agent-integration.md @@ -95,13 +95,25 @@ POST /v1/ramp/register X-API-Key: sk_* ``` -Before calling register, **generate per-chain ephemeral accounts** for the chains involved in the route: - -- EVM legs → a fresh secp256k1 keypair. -- Substrate legs (Pendulum, AssetHub, Moonbeam, Hydration) → fresh sr25519 keypairs. -- Stellar legs → a fresh Ed25519 keypair. +Before calling register, **generate the two ephemeral accounts** the ramp uses. The API accepts exactly two account types, and one account of each covers every leg of the route: + +- One EVM account → a fresh secp256k1 keypair (used for all EVM legs). +- One Substrate account → a fresh sr25519 keypair (used for all Substrate legs: Pendulum, AssetHub, Moonbeam, Hydration). + +Send **only the public addresses**, in the `signingAccounts` array of the register body: + +```json +{ + "quoteId": "QUOTE_ID", + "signingAccounts": [ + { "type": "Substrate", "address": "5..." }, + { "type": "EVM", "address": "0x..." } + ], + "additionalData": { "destinationAddress": "0x..." } +} +``` -Send **only the public addresses** in the register request. Persist the secret keys to your secure store, keyed by the not-yet-issued ramp; once the response returns a `rampId`, rekey the store entry. Never log the secrets. +`type` must be exactly `"Substrate"` or `"EVM"`; no other value is accepted. There is no `publicKey` field on register — partner attribution rides on the quote's `apiKey`. Persist the secret keys to your secure store, keyed by the not-yet-issued ramp; once the response returns a `rampId`, rekey the store entry. Never log the secrets. The response contains: @@ -109,7 +121,7 @@ The response contains: - current ramp state and phase - `unsignedTxs` — an ordered list of transactions to sign -Each unsigned transaction declares its `network`, `signer` address, transaction format (`evm-transaction`, `evm-typed-data`, `substrate-extrinsic`, `stellar-transaction`), and the payload bytes or fields to sign. +Each unsigned transaction declares its `network`, `signer` address, transaction format (`evm-transaction`, `evm-typed-data`, or `substrate-extrinsic`), and the payload bytes or fields to sign. ### D.4 Sign And Update From 116a4a6e775d3ff807988005276c60d544819799 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 13 Jul 2026 10:02:19 +0200 Subject: [PATCH 4/8] docs(api): fix Moonbeam account type, drop Stellar from OpenAPI, clarify presigned count Second copilot review pass on PR #1266, verified against the backend: - Moonbeam is an EVM chain (createMoonbeamEphemeral -> EVM ephemeral; normalizeAndValidateSigningAccounts routes it as EVM). The AI-agent page grouped it under Substrate; move it to the EVM account. - Remove the vestigial "Stellar" signingAccount type from the OpenAPI schema (3 enum sites) and regenerate vortex.openapi.d.ts. The backend only recognizes Substrate/EVM and silently ignores any other type; reword the affected schema descriptions (also dropping stale taxId requirement text) to match. Softens the AI-agent page wording to "only recognized values ... any other type is ignored". - Correct the skill's "5 presigned transactions per ramp" claim: the count is per ephemeral-signed transaction (1 primary + 4 backups in meta.additionalTxs), and a ramp can contain several such transactions. --- .agents/skills/vortex-integration/SKILL.md | 2 +- docs/api/openapi/vortex.openapi.d.ts | 32 ++++++---------------- docs/api/openapi/vortex.openapi.json | 14 +++++----- docs/api/pages/12-ai-agent-integration.md | 6 ++-- 4 files changed, 20 insertions(+), 34 deletions(-) diff --git a/.agents/skills/vortex-integration/SKILL.md b/.agents/skills/vortex-integration/SKILL.md index ce68fea36..11df4df00 100644 --- a/.agents/skills/vortex-integration/SKILL.md +++ b/.agents/skills/vortex-integration/SKILL.md @@ -21,7 +21,7 @@ A machine-loadable capability catalog for AI coding agents integrating Vortex in - **Ramp registration requires a user-linked `sk_*` key in every corridor** — the register call is rejected unless the authenticated key resolves to a user account. KYC identity (BRL tax ID, Alfredpay customer, Mykobo customer) is derived from that account, never from request fields. - **Decimals**: all amounts are strings. Never parse them through JS `Number` — use `BigInt`, `decimal.js`, or equivalent. - **Quote TTL**: quotes expire (see `expiresAt`). Re-quote, never reuse stale quotes. -- **Ramp counts**: ephemeral keys sign exactly 5 presigned transactions per ramp. The API rejects anything else. +- **Presigned counts**: this is **per ephemeral-signed transaction, not per ramp**. Each transaction an ephemeral key signs must be submitted as 5 presigned variants — 1 primary plus exactly 4 backups with consecutive nonces in `meta.additionalTxs` (`NUMBER_OF_PRESIGNED_TXS = 5`); the API rejects any other backup count. A ramp can contain several ephemeral-signed transactions across its phases. (The SDK builds these for you; only raw-API integrations need to construct them.) - **Currently implemented corridors** (all live in the SDK): BRL via PIX, EUR via SEPA (Mykobo), USD via ACH, MXN via SPEI, COP via ACH, ARS via CBU. All support both onramp (BUY) and offramp (SELL). EUR and the bank-transfer corridors deliver to EVM networks only (no AssetHub). - **EUR enum value**: EUR quotes use `FiatToken.EURC` (not `EUR`) as the currency value, with `"sepa"` as the rail identifier. - **taxId is deprecated for BRL**: the user's tax ID is derived server-side from the user-linked `sk_*` key. Sending a `taxId` that mismatches the derived one is rejected; stop sending it in new integrations. diff --git a/docs/api/openapi/vortex.openapi.d.ts b/docs/api/openapi/vortex.openapi.d.ts index 64b28394a..6547c6652 100644 --- a/docs/api/openapi/vortex.openapi.d.ts +++ b/docs/api/openapi/vortex.openapi.d.ts @@ -1097,7 +1097,7 @@ export interface components { * @description The type of the account. * @enum {string} */ - type: "EVM" | "Stellar" | "Substrate"; + type: "EVM" | "Substrate"; }; /** @enum {string} */ AveniaDocumentType: "ID" | "DRIVERS-LICENSE" | "PASSPORT" | "SELFIE" | "SELFIE-FROM-LIVENESS"; @@ -1559,11 +1559,9 @@ export interface components { /** * @description Optional additional data for the ramp process. * - * For Stellar offramps, paymentData is required. + * For Brazil onramps, destinationAddress is required. * - * For Brazil onramps, destinationAddress and taxId arerequired. - * - * For Brazil offramps, pixDestination, taxId and receiverTaxId are required. + * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. */ additionalData?: { /** @description Destination address, used for onramp. */ @@ -1587,12 +1585,7 @@ export interface components { * @description The unique identifier for the quote. */ quoteId: string; - /** - * @description Array of accounts that will be used for signing transactions. - * - * For Stellar offramps, Stellar and Pendulum ephemerals are required. - * For Brazil on/off ramps, Moonbeam and Pendulum ephemerals are required. - */ + /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ signingAccounts: { /** @description The account address. */ address: string; @@ -1600,7 +1593,7 @@ export interface components { * @description The type of the account. * @enum {string} */ - type: "EVM" | "Stellar" | "Substrate"; + type: "EVM" | "Substrate"; }[]; }; /** @description `PENDING`, `FAILED`, `COMPLETED` */ @@ -2710,11 +2703,9 @@ export interface operations { /** * @description Optional additional data for the ramp process. * - * For Stellar offramps, paymentData is required. + * For Brazil onramps, destinationAddress is required. * - * For Brazil onramps, destinationAddress and taxId arerequired. - * - * For Brazil offramps, pixDestination, taxId and receiverTaxId are required. + * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. */ additionalData?: { /** @description Destination address, used for onramp. */ @@ -2739,12 +2730,7 @@ export interface operations { * @description The unique identifier for the quote. */ quoteId: string; - /** - * @description Array of accounts that will be used for signing transactions. - * - * For Stellar offramps, Stellar and Pendulum ephemerals are required. - * For Brazil on/off ramps, Moonbeam and Pendulum ephemerals are required. - */ + /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ signingAccounts: { /** @description The account address. */ address: string; @@ -2752,7 +2738,7 @@ export interface operations { * @description The type of the account. * @enum {string} */ - type: "EVM" | "Stellar" | "Substrate"; + type: "EVM" | "Substrate"; }[]; }; }; diff --git a/docs/api/openapi/vortex.openapi.json b/docs/api/openapi/vortex.openapi.json index 7f5845a30..9635575e6 100644 --- a/docs/api/openapi/vortex.openapi.json +++ b/docs/api/openapi/vortex.openapi.json @@ -49,7 +49,7 @@ }, "type": { "description": "The type of the account.", - "enum": ["EVM", "Stellar", "Substrate"], + "enum": ["EVM", "Substrate"], "type": "string" } }, @@ -1100,7 +1100,7 @@ "properties": { "additionalData": { "additionalProperties": true, - "description": "Optional additional data for the ramp process.\n\nFor Stellar offramps, paymentData is required.\n\nFor Brazil onramps, destinationAddress and taxId arerequired.\n\nFor Brazil offramps, pixDestination, taxId and receiverTaxId are required.", + "description": "Optional additional data for the ramp process.\n\nFor Brazil onramps, destinationAddress is required.\n\nFor Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID.", "properties": { "destinationAddress": { "description": "Destination address, used for onramp.", @@ -1139,7 +1139,7 @@ "type": "string" }, "signingAccounts": { - "description": "Array of accounts that will be used for signing transactions.\n\nFor Stellar offramps, Stellar and Pendulum ephemerals are required.\nFor Brazil on/off ramps, Moonbeam and Pendulum ephemerals are required.\n", + "description": "Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account.\n", "items": { "properties": { "address": { @@ -1148,7 +1148,7 @@ }, "type": { "description": "The type of the account.", - "enum": ["EVM", "Stellar", "Substrate"], + "enum": ["EVM", "Substrate"], "type": "string" } }, @@ -3110,7 +3110,7 @@ "properties": { "additionalData": { "additionalProperties": true, - "description": "Optional additional data for the ramp process.\n\nFor Stellar offramps, paymentData is required.\n\nFor Brazil onramps, destinationAddress and taxId arerequired.\n\nFor Brazil offramps, pixDestination, taxId and receiverTaxId are required.", + "description": "Optional additional data for the ramp process.\n\nFor Brazil onramps, destinationAddress is required.\n\nFor Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID.", "properties": { "destinationAddress": { "description": "Destination address, used for onramp.", @@ -3152,7 +3152,7 @@ "type": "string" }, "signingAccounts": { - "description": "Array of accounts that will be used for signing transactions.\n\nFor Stellar offramps, Stellar and Pendulum ephemerals are required.\nFor Brazil on/off ramps, Moonbeam and Pendulum ephemerals are required.\n", + "description": "Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account.\n", "items": { "properties": { "address": { @@ -3161,7 +3161,7 @@ }, "type": { "description": "The type of the account.", - "enum": ["EVM", "Stellar", "Substrate"], + "enum": ["EVM", "Substrate"], "type": "string" } }, diff --git a/docs/api/pages/12-ai-agent-integration.md b/docs/api/pages/12-ai-agent-integration.md index 6a9c5ee2d..e0117e24a 100644 --- a/docs/api/pages/12-ai-agent-integration.md +++ b/docs/api/pages/12-ai-agent-integration.md @@ -97,8 +97,8 @@ X-API-Key: sk_* Before calling register, **generate the two ephemeral accounts** the ramp uses. The API accepts exactly two account types, and one account of each covers every leg of the route: -- One EVM account → a fresh secp256k1 keypair (used for all EVM legs). -- One Substrate account → a fresh sr25519 keypair (used for all Substrate legs: Pendulum, AssetHub, Moonbeam, Hydration). +- One EVM account → a fresh secp256k1 keypair (used for all EVM legs, including Moonbeam — Moonbeam is an EVM chain and takes an `EVM` signing account). +- One Substrate account → a fresh sr25519 keypair (used for all Substrate legs: Pendulum, AssetHub, Hydration). Send **only the public addresses**, in the `signingAccounts` array of the register body: @@ -113,7 +113,7 @@ Send **only the public addresses**, in the `signingAccounts` array of the regist } ``` -`type` must be exactly `"Substrate"` or `"EVM"`; no other value is accepted. There is no `publicKey` field on register — partner attribution rides on the quote's `apiKey`. Persist the secret keys to your secure store, keyed by the not-yet-issued ramp; once the response returns a `rampId`, rekey the store entry. Never log the secrets. +`type` must be `"Substrate"` or `"EVM"` — these are the only recognized values, and any other type is ignored. There is no `publicKey` field on register — partner attribution rides on the quote's `apiKey`. Persist the secret keys to your secure store, keyed by the not-yet-issued ramp; once the response returns a `rampId`, rekey the store entry. Never log the secrets. The response contains: From c8708c07c6c5c995d14863e34a03ebd805b968bc Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 13 Jul 2026 10:16:21 +0200 Subject: [PATCH 5/8] docs: add codebase map and layered per-workspace CLAUDE.md Add MAP.md as a root wayfinding reference and scoped CLAUDE.md files for each app/package (api, frontend, rebalancer, shared, sdk). Relocate app-specific architecture, commands, and conventions out of the root CLAUDE.md into the relevant subdirectory so agents load only applicable rules, and slim the root to cross-cutting context with links down. Applies the 'lean + layered CLAUDE.md' and 'codebase map' strategies from the large-codebase playbook. --- CLAUDE.md | 208 +++++++++++++------------------------- MAP.md | 47 +++++++++ apps/api/CLAUDE.md | 50 +++++++++ apps/frontend/CLAUDE.md | 51 ++++++++++ apps/rebalancer/CLAUDE.md | 16 +++ packages/sdk/CLAUDE.md | 30 ++++++ packages/shared/CLAUDE.md | 27 +++++ 7 files changed, 291 insertions(+), 138 deletions(-) create mode 100644 MAP.md create mode 100644 apps/api/CLAUDE.md create mode 100644 apps/frontend/CLAUDE.md create mode 100644 apps/rebalancer/CLAUDE.md create mode 100644 packages/sdk/CLAUDE.md create mode 100644 packages/shared/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index fd22663b7..cbcba020d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,179 +1,110 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Guidance for Claude Code (claude.ai/code) working in this repository. This root file +holds **cross-cutting** context only. Each app/package has its own `CLAUDE.md` with +scoped architecture and commands — `cd` into the relevant one before working there, and +read it first. ## Project Overview -Vortex is a cross-border payments gateway built on the Pendulum blockchain. It enables on-ramping and off-ramping of fiat currencies through stablecoins using cross-chain swaps via XCM (Cross-Consensus Messaging). +Vortex is a cross-border payments gateway built on the Pendulum blockchain. It enables +on-ramping and off-ramping of fiat currencies through stablecoins using cross-chain swaps +via XCM (Cross-Consensus Messaging). -## Monorepo Structure +## Repository Map -This is a **Bun monorepo** using workspaces: +Full wayfinding is in [`MAP.md`](MAP.md). This is a **Bun monorepo** using workspaces: -- **apps/frontend** - React 19 + Vite web application -- **apps/api** - Express backend service (PostgreSQL + Sequelize) -- **apps/rebalancer** - Liquidity rebalancing service -- **packages/shared** - Shared utilities, types, token configs, and helpers -- **packages/sdk** - Public SDK for Vortex API integration +- **apps/frontend** — React 19 + Vite web app → [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) +- **apps/api** — Express backend (PostgreSQL + Sequelize) → [`apps/api/CLAUDE.md`](apps/api/CLAUDE.md) +- **apps/rebalancer** — liquidity rebalancing service → [`apps/rebalancer/CLAUDE.md`](apps/rebalancer/CLAUDE.md) +- **packages/shared** — `@vortexfi/shared` utilities/configs → [`packages/shared/CLAUDE.md`](packages/shared/CLAUDE.md) +- **packages/sdk** — `@vortexfi/sdk` public SDK → [`packages/sdk/CLAUDE.md`](packages/sdk/CLAUDE.md) -## Essential Commands +## Monorepo Commands -> Always use `bun`- never `npm`, `yarn`, or `pnpm`. Run `bun lint:fix` after any code change. +> Always use `bun` — never `npm`, `yarn`, or `pnpm`. Run `bun lint:fix` after any code +> change. Per-app test/dev/migrate commands live in each subdirectory's `CLAUDE.md`. ```bash -# Install all dependencies -bun install - -# Development (runs frontend, backend, and shared concurrently) -bun dev - -# Individual app development -bun dev:frontend # Frontend at http://127.0.0.1:5173 -bun dev:backend # Backend at http://localhost:3000 +bun install # install all dependencies +bun dev # frontend + backend + shared concurrently +bun dev:frontend # http://127.0.0.1:5173 +bun dev:backend # http://localhost:3000 bun dev:rebalancer -# Build -bun build # Build all (shared -> sdk -> frontend -> backend) -bun build:shared # Must build shared first when making changes -bun build:frontend -bun build:backend - -# Linting and formatting (uses Biome) -bun lint # Run linter -bun lint:fix # Auto-fix lint issues -bun format # Format all files -bun verify # Check without fixing - -# Type checking -bun typecheck - -# Database (from apps/api) -cd apps/api -bun migrate # Run migrations -bun migrate:revert # Revert all migrations -bun migrate:revert-last # Revert last migration -bun seed:phase-metadata # Seed phase configuration - -# Testing -cd apps/frontend && bun test # Frontend tests (Vitest) -cd apps/api && bun test # Backend tests -``` - -## Architecture - -### State Machine Pattern - -The ramping process uses a state machine with defined phases: -- **Offramp**: prepareTransactions → squidRouter → pendulumFundEphemeral → subsidizePreSwap → nablaApprove → nablaSwap → subsidizePostSwap → performBrlaPayout → pendulumCleanup -- **Onramp**: brlaTeleport → createMoonbeamEphemeral → executeMoonbeamToPendulumXCM → subsidizePreSwap → nablaApprove → nablaSwap → executePendulumToAssetHubXCM → pendulumCleanup - -Phase metadata and valid transitions are stored in PostgreSQL and seeded via `seed:phase-metadata`. - -### Frontend Architecture - -- **State**: Zustand stores (`stores/`) + React Context (`contexts/`) -- **Forms**: React Hook Form with Zod validation (not Yup) -- **Data Fetching**: TanStack Query -- **Routing**: TanStack Router (route tree auto-generated in `routeTree.gen.ts`) -- **State Machines**: XState machines in `machines/` for complex flows (KYC, ramp process) -- **Wallet Integration**: Wagmi/AppKit (EVM) + Talisman (Polkadot) - -### Backend Architecture - -- **API Layer**: Express routes in `api/routes/`, controllers in `api/controllers/` -- **Services**: Business logic in `api/services/` -- **Models**: Sequelize models in `models/` (RampState, QuoteTicket, Partner, etc.) -- **Workers**: Background jobs in `api/workers/` -- **Cross-chain**: XCM handlers, Nabla AMM integration, Stellar/BRLA APIs - -### Shared Package (`@vortexfi/shared`) - -Contains cross-package utilities: -- Token configurations and network definitions -- Endpoint helpers for API calls -- Contract ABIs and addresses -- Decimal/BigNumber helpers -- Logger configuration - -**Important**: Always rebuild shared when making changes: `bun build:shared` +bun build # build all (shared -> sdk -> frontend -> backend) +bun build:shared # rebuild shared (see below) -After ANY change to `packages/shared`, run `bun build:shared` before running frontend/api. - -## Code Style Guidelines - -From `.clinerules/`: - -### General -- Prefer composition over inheritance -- Create ADRs in `/docs/adr` for major architectural changes - -### Frontend-Specific -- Avoid `useState` unless absolutely needed; prefer derived data and `useRef` -- Avoid `useEffect` except for external system synchronization -- Avoid `setTimeout` (always comment why if used) -- Extract complex conditional rendering into new components -- Skip useless comments; only comment race conditions, TODOs, or genuinely confusing code +bun lint # Biome lint bun lint:fix # auto-fix +bun format # format all bun verify # check without fixing +bun typecheck # type check +``` -### XState v5 -- Use `setup({ ... }).createMachine(...)` API- not `createMachine` directly -- Actor refs from `useActor` / `useSelector` from `@xstate/react` -- Machine files live in `apps/frontend/src/machines/` +### Always rebuild shared after changing it -### Biome Configuration -- Line width: 128 -- Indent: 2 spaces -- Semicolons: always -- Trailing commas: none -- Quote style: double -- Sorted Tailwind classes enforced via `useSortedClasses` rule +`packages/shared` is consumed as built output. **After ANY change to `packages/shared`, +run `bun build:shared` before running frontend/api** — otherwise they use stale code. ## Token Exhaustiveness `FiatToken` currently has 6 values: `EURC`, `ARS`, `BRL`, `USD`, `MXN`, `COP`. Any `Record` must include ALL six. Missing entries cause TypeScript errors -when shared is rebuilt. Check: tokenAvailability, mapFiatToDestination, success page -ARRIVAL_TEXT_BY_TOKEN, sep10 tokenMapping. +when shared is rebuilt. Check: `tokenAvailability`, `mapFiatToDestination`, success page +`ARRIVAL_TEXT_BY_TOKEN`, sep10 `tokenMapping`. + +## Code Style + +Biome config: line width 128, 2-space indent, semicolons always, no trailing commas, +double quotes, sorted Tailwind classes (`useSortedClasses`). General: prefer composition +over inheritance; create ADRs in `/docs/adr` for major architectural changes. +Frontend-specific and XState conventions live in +[`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md). ## No Over-Engineering -- Don't add features, refactors, or "improvements" beyond what was asked -- Don't add docstrings/comments to code you didn't touch -- Don't create helpers/utilities for one-time operations -- Don't validate inputs that can't be invalid (internal calls, typed params) -- Three similar lines is better than a premature abstraction +- Don't add features, refactors, or "improvements" beyond what was asked. +- Don't add docstrings/comments to code you didn't touch. +- Don't create helpers/utilities for one-time operations. +- Don't validate inputs that can't be invalid (internal calls, typed params). +- Three similar lines is better than a premature abstraction. ## Testing -### Test Coverage Requirements - These apply to every agent working in this repo: -- **Bug fixes and regressions**: if a bug or regression slipped past the existing tests, add a test that reproduces it (and fails without the fix) before/with the fix — so it can't silently come back. Write the test at the level that actually covers the gap (unit or integration). Only skip when a test genuinely can't capture it (e.g. purely cosmetic, environment/config, or third-party behavior) — and say why you skipped. -- **New features**: always ship the appropriate tests alongside the feature. Cover the core behavior and the edge cases that matter, not just the happy path. - -### Backend Integration Tests -```bash -cd apps/api -bun test phase-processor.integration.test.ts --timeout X -``` -State is stored in `lastRampState.json`. For recovery testing, copy failed state to `failedRampStateRecovery.json` and run the recovery test. +- **Bug fixes and regressions**: if a bug slipped past existing tests, add a test that + reproduces it (and fails without the fix) before/with the fix, so it can't silently + come back. Write it at the level that covers the gap (unit or integration). Only skip + when a test genuinely can't capture it (purely cosmetic, environment/config, or + third-party behavior) — and say why you skipped. +- **New features**: always ship the appropriate tests alongside the feature. Cover the + core behavior and the edge cases that matter, not just the happy path. -### Frontend Tests -```bash -cd apps/frontend -bun test -``` +Per-app test commands and integration-test state handling live in each subdirectory's +`CLAUDE.md`. ## Security Spec Sync -`docs/security-spec/` is the audit-facing source of truth for security-sensitive behavior, and it must not go stale. Any change to an API feature or its business logic — auth, admin routes, quote/ramp state, signing, fees, partner pricing, integrations, migrations/schema that affect invariants, or cross-chain fund flow — must be cross-checked against the matching spec file and updated in the same change whenever the behavior it documents changed. This applies to every agent working in this repo, not just the one that first touched the code. +`docs/security-spec/` is the audit-facing source of truth for security-sensitive +behavior, and it must not go stale. Any change to an API feature or its business logic — +auth, admin routes, quote/ramp state, signing, fees, partner pricing, integrations, +migrations/schema that affect invariants, or cross-chain fund flow — must be cross-checked +against the matching spec file and updated in the same change whenever the behavior it +documents changed. This applies to every agent working in this repo. -Keep this lightweight: grep/read only the relevant spec path from `docs/security-spec/README.md`; skip this for cosmetic refactors, test-only changes, or implementation changes that do not alter security-relevant behavior. If a change alters documented behavior but you are unsure which spec file owns it, say so rather than leaving the spec silently stale. +Keep this lightweight: grep/read only the relevant spec path from +`docs/security-spec/README.md`; skip it for cosmetic refactors, test-only changes, or +implementation changes that do not alter security-relevant behavior. If a change alters +documented behavior but you are unsure which spec file owns it, say so rather than leaving +the spec silently stale. ## Type Issues -If IDE doesn't detect `@pendulum-chain/types` properly, ensure all `@polkadot/*` packages match versions in the types package. The root `package.json` uses `catalog:` for version management. +If the IDE doesn't detect `@pendulum-chain/types` properly, ensure all `@polkadot/*` +packages match versions in the types package. The root `package.json` uses `catalog:` for +version management. --- @@ -233,4 +164,5 @@ For multi-step tasks, state a brief plan: 3. [Step] → verify: [check] ``` -Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. +Strong success criteria let you loop independently. Weak criteria ("make it work") +require constant clarification. diff --git a/MAP.md b/MAP.md new file mode 100644 index 000000000..1178c030a --- /dev/null +++ b/MAP.md @@ -0,0 +1,47 @@ +# Repository Map + +Wayfinding for the Vortex monorepo: what kind of code lives where. Each app/package +has its own `CLAUDE.md` with scoped commands and conventions — `cd` into the relevant +one before working there. + +## Apps + +| Path | What lives here | +|------|-----------------| +| `apps/frontend` | React 19 + Vite web app — the user-facing ramp UI. Zustand, TanStack Query/Router, XState, Wagmi/Talisman wallets. → [`CLAUDE.md`](apps/frontend/CLAUDE.md) | +| `apps/api` | Express backend (PostgreSQL + Sequelize). Ramp state machine, quotes, partners, webhooks, XCM/Nabla/Stellar/BRLA integrations. → [`CLAUDE.md`](apps/api/CLAUDE.md) | +| `apps/rebalancer` | Standalone liquidity rebalancing service. → [`CLAUDE.md`](apps/rebalancer/CLAUDE.md) | + +## Packages + +| Path | What lives here | +|------|-----------------| +| `packages/shared` | `@vortexfi/shared` — token/network configs, contract ABIs & addresses, decimal/BigNumber helpers, endpoint helpers, logger. Consumed by every app. → [`CLAUDE.md`](packages/shared/CLAUDE.md) | +| `packages/sdk` | `@vortexfi/sdk` — the public integration SDK shipped to partners. → [`CLAUDE.md`](packages/sdk/CLAUDE.md) | + +## Contracts + +| Path | What lives here | +|------|-----------------| +| `contracts` | Solidity contracts: `cctp-settlement` and `relayer`. | +| `relayer-contract` | Relayer contract security-audit material. | + +## Docs & specs + +| Path | What lives here | +|------|-----------------| +| `docs/security-spec` | Audit-facing source of truth for security-sensitive behavior. Keep in sync with code changes (see root `CLAUDE.md` → Security Spec Sync). | +| `docs/api` | Public API docs — OpenAPI spec (`openapi/`) and prose pages (`pages/`). Whitelabeled. | +| `docs/architecture`, `docs/features`, `docs/qa`, `docs/refactoring` | Architecture notes, feature write-ups, QA and refactoring records. | +| `docs/testing-strategy.md`, `docs/test-audit-findings.md` | Testing approach and audit findings. | +| `memory-bank` | Long-form project context: product/tech context, decision log, phases, progress. | + +## Tooling & config + +| Path | What lives here | +|------|-----------------| +| `scripts` | Repo tooling — `check-coverage.ts`, `coverage-report.ts` (LCOV-based coverage gate). | +| `supabase` | Supabase config, DB migrations, snippets, email templates. | +| `.agents/skills` | Repo-scoped agent skills: `vortex-integration` (partner integration recipes), `sentry-vortex` (frontend error-instrumentation audit). | +| `.clinerules` | Cline coding rules (general, useful prompts, frontend). | +| `.claude` | Claude Code config — shared `settings.json` (deny rules), personal `settings.local.json` (ignored), worktrees. | diff --git a/apps/api/CLAUDE.md b/apps/api/CLAUDE.md new file mode 100644 index 000000000..631def2a1 --- /dev/null +++ b/apps/api/CLAUDE.md @@ -0,0 +1,50 @@ +# apps/api — Vortex backend + +Express + PostgreSQL/Sequelize service. Root `CLAUDE.md` holds cross-cutting rules +(over-engineering, security-spec sync, testing policy); this file holds API-scoped +architecture and commands. Run commands from `apps/api/` unless noted. + +## Architecture + +- **API layer**: routes in `src/api/routes/`, controllers in `src/api/controllers/`. +- **Services**: business logic in `src/api/services/`. +- **Models**: Sequelize models in `src/models/` (RampState, QuoteTicket, Partner, …). +- **Workers**: background jobs in `src/api/workers/`. +- **Cross-chain**: XCM handlers, Nabla AMM integration, Stellar/BRLA APIs. +- **Middlewares / observability / errors / helpers**: under `src/api/`. + +### Ramp state machine + +The ramping process runs through defined phases; metadata and valid transitions live in +PostgreSQL, seeded via `bun seed:phase-metadata`. + +- **Offramp**: prepareTransactions → squidRouter → pendulumFundEphemeral → subsidizePreSwap → nablaApprove → nablaSwap → subsidizePostSwap → performBrlaPayout → pendulumCleanup +- **Onramp**: brlaTeleport → createMoonbeamEphemeral → executeMoonbeamToPendulumXCM → subsidizePreSwap → nablaApprove → nablaSwap → executePendulumToAssetHubXCM → pendulumCleanup + +## Commands (from `apps/api/`) + +```bash +bun test # full backend suite +bun test # single file, e.g. bun test ramp.service.test.ts +bun test phase-processor.integration.test.ts --timeout X # integration test + +bun migrate # run migrations +bun migrate:revert # revert ALL migrations (destructive — dev only) +bun migrate:revert-last # revert last migration +bun seed:phase-metadata # seed phase configuration +``` + +Lint with the repo Biome config: from repo root `bun lint:fix`, or target a path with +`bunx @biomejs/biome check apps/api/src`. Dev server: `bun dev:backend` from root +(backend at http://localhost:3000). + +## Integration test state + +Integration tests store state in `lastRampState.json`. For recovery testing, copy a +failed state into `failedRampStateRecovery.json` and run the recovery test. + +## Security spec + +Changes to auth, admin routes, quote/ramp state, signing, fees, partner pricing, +integrations, or migrations that affect invariants must be cross-checked against +`docs/security-spec/` in the same change. See root `CLAUDE.md` → Security Spec Sync. diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md new file mode 100644 index 000000000..8a63872b6 --- /dev/null +++ b/apps/frontend/CLAUDE.md @@ -0,0 +1,51 @@ +# apps/frontend — Vortex web app + +React 19 + Vite. Root `CLAUDE.md` holds cross-cutting rules; this file holds +frontend-scoped architecture, conventions, and commands. Run commands from +`apps/frontend/` unless noted. + +## Architecture + +- **State**: Zustand stores (`src/stores/`) + React Context (`src/contexts/`). +- **Forms**: React Hook Form with Zod validation (not Yup). +- **Data fetching**: TanStack Query. +- **Routing**: TanStack Router — route tree auto-generated in `src/routeTree.gen.ts` (do not hand-edit). +- **State machines**: XState machines in `src/machines/` for complex flows (KYC, ramp process). +- **Wallets**: Wagmi/AppKit (EVM) + Talisman (Polkadot). + +## Conventions + +- Avoid `useState` unless truly needed; prefer derived data and `useRef`. +- Avoid `useEffect` except for external-system synchronization. +- Avoid `setTimeout` (always comment why if used). +- Extract complex conditional rendering into new components. +- Skip useless comments; only comment race conditions, TODOs, or genuinely confusing code. + +### XState v5 + +- Use `setup({ ... }).createMachine(...)` — not `createMachine` directly. +- Get actor refs via `useActor` / `useSelector` from `@xstate/react`. + +## Commands (from `apps/frontend/`) + +```bash +bun test # Vitest suite +bun test # single file / pattern +``` + +Dev server: `bun dev:frontend` from root (http://127.0.0.1:5173). Lint from root with +`bun lint:fix`, or `bunx @biomejs/biome check apps/frontend/src`. + +## Error instrumentation + +Sentry conventions here are strict (single-source filtering, no capture in components, +no PII). Before adding error handling, API service methods, or machine error states, +follow the **`sentry-vortex`** skill (`.agents/skills/sentry-vortex/SKILL.md`) — it is +the authority for correct instrumentation in this app. + +## Token exhaustiveness + +`FiatToken` has 6 values (`EURC`, `ARS`, `BRL`, `USD`, `MXN`, `COP`). Any +`Record` must include all six or the build fails. Common spots: +`tokenAvailability`, `mapFiatToDestination`, success-page `ARRIVAL_TEXT_BY_TOKEN`, +sep10 `tokenMapping`. diff --git a/apps/rebalancer/CLAUDE.md b/apps/rebalancer/CLAUDE.md new file mode 100644 index 000000000..4643aa79f --- /dev/null +++ b/apps/rebalancer/CLAUDE.md @@ -0,0 +1,16 @@ +# apps/rebalancer — liquidity rebalancing service + +Standalone service that rebalances liquidity across chains. Root `CLAUDE.md` holds +cross-cutting rules. Run commands from `apps/rebalancer/`. + +## Commands + +```bash +bun run src/index.ts # run the service (also: bun dev:rebalancer from root) +bun test # test suite +``` + +Lint from root with `bun lint:fix`, or `bunx @biomejs/biome check apps/rebalancer/src`. + +Depends on `@vortexfi/shared` — after changing `packages/shared`, run `bun build:shared` +(from root) before running this service. diff --git a/packages/sdk/CLAUDE.md b/packages/sdk/CLAUDE.md new file mode 100644 index 000000000..94dca9406 --- /dev/null +++ b/packages/sdk/CLAUDE.md @@ -0,0 +1,30 @@ +# packages/sdk — @vortexfi/sdk + +The **public** integration SDK shipped to partners. It is consumed outside this repo, so +treat its API surface as a stable contract — breaking changes ripple to integrators. + +## SDK-specific gotchas + +- **Lint uses ESLint, not Biome**: `bun lint` runs `eslint . --ext .ts`. The repo-wide + `bun lint:fix` (Biome) does not govern this package's lint. +- **`test` also builds and smoke-loads the dist**: `bun test` runs the suite, then + `bun run build`, then `node -e "require('./dist/index.js')"`. A green `bun test` means + the built bundle imports cleanly too. +- **Dual build**: see `ARCHITECTURE.md`, `DUAL_BUILD_GUIDE.md`, and + `CHANGELOG_DUAL_BUILD.md` before touching the build config. + +## Commands (from `packages/sdk/`) + +```bash +bun test # suite + build + dist smoke-load +bun run build # tsc types + bundle to dist/ +bun typecheck # bun x --bun tsc +bun lint # eslint +``` + +## Integration recipes + +Partner-facing usage patterns (quotes, on/off-ramp flows, webhooks, auth, error +recovery) are documented in the **`vortex-integration`** skill +(`.agents/skills/vortex-integration/SKILL.md`). Keep that skill in sync when the SDK's +public surface changes. diff --git a/packages/shared/CLAUDE.md b/packages/shared/CLAUDE.md new file mode 100644 index 000000000..2c74e2431 --- /dev/null +++ b/packages/shared/CLAUDE.md @@ -0,0 +1,27 @@ +# packages/shared — @vortexfi/shared + +Cross-package utilities consumed by every app: token/network configs, contract ABIs & +addresses, decimal/BigNumber helpers, endpoint helpers, logger. + +## Rebuild after every change + +`@vortexfi/shared` is built to `dist/` (dual node + browser targets). Consumers read the +built output, not `src/`. **After ANY change here, run `bun build:shared` (from root)** +before running or testing frontend/api — otherwise they use stale code. + +```bash +bun test # from packages/shared/ +bun build:shared # from root — rebuild node + browser bundles +``` + +## Token exhaustiveness + +`FiatToken` has 6 values (`EURC`, `ARS`, `BRL`, `USD`, `MXN`, `COP`). Any +`Record` defined here must include all six, or dependents fail to typecheck +when shared is rebuilt. + +## Type resolution + +If `@pendulum-chain/types` isn't detected properly, ensure all `@polkadot/*` packages +match the versions in the types package. The root `package.json` manages versions via +`catalog:`. From f89029d25a5a1745be62e9fe080839c35a01d287 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 13 Jul 2026 10:16:34 +0200 Subject: [PATCH 6/8] chore: add version-controlled Claude Code permission denies Add .claude/settings.json with deny rules for force-push and reads of secret files (.env*, .api-key.json). Change .gitignore from ignoring all of .claude to .claude/* with a negation for settings.json, so the team-shared denies are version-controlled while settings.local.json and worktrees/ stay ignored. --- .claude/settings.json | 15 +++++++++++++++ .gitignore | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..62ac24011 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "$comment": "Team-shared, version-controlled Claude Code settings. Personal overrides go in the git-ignored settings.local.json. deny takes precedence over any allow.", + "permissions": { + "deny": [ + "Bash(git push --force*)", + "Bash(git push -f *)", + "Bash(git push origin --force*)", + "Read(./.env)", + "Read(./.env.*)", + "Read(**/.env)", + "Read(**/.env.*)", + "Read(./.api-key.json)" + ] + } +} diff --git a/.gitignore b/.gitignore index 64e43d4ec..f2e8deaca 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,9 @@ storybook-static CLAUDE.local.md -.claude +# Ignore everything under .claude except the team-shared settings.json +.claude/* +!.claude/settings.json /.roo/* # hardhat generated files in workspace contract projects From 4417d184d4c41258d92a67c767910e315cf6b8b6 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 13 Jul 2026 10:16:41 +0200 Subject: [PATCH 7/8] docs(skills): scope sentry-vortex skill to apps/frontend Sharpen the skill description to name its apps/frontend scope so it surfaces when working in the React app and not elsewhere. The frontend and sdk CLAUDE.md files (previous commit) reference their governing skills, giving path-scoped skill discovery via the directory-scoped instructions. --- .agents/skills/sentry-vortex/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/sentry-vortex/SKILL.md b/.agents/skills/sentry-vortex/SKILL.md index bd8942fe1..e4382e6fb 100644 --- a/.agents/skills/sentry-vortex/SKILL.md +++ b/.agents/skills/sentry-vortex/SKILL.md @@ -1,6 +1,6 @@ --- name: sentry-vortex -description: Audit code against Vortex's Sentry conventions and guide correct error instrumentation. Triggers on: sentry, captureException, error reporting, error monitoring, beforeSend, ignoreErrors, adding an API service method, new error class, new XState machine error handling, ErrorBoundary, "is this reported to Sentry". +description: "Scoped to apps/frontend (the React web app). Audit code against Vortex's Sentry conventions and guide correct error instrumentation. Triggers when working under apps/frontend on: sentry, captureException, error reporting, error monitoring, beforeSend, ignoreErrors, adding an API service method, new error class, new XState machine error handling, ErrorBoundary, \"is this reported to Sentry\"." user-invocable: true argument-hint: "[files or 'current changes' — defaults to the working-tree diff]" --- From 299c9c5ca9c44ecc5be2b2e286edfd524dc9a971 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Mon, 13 Jul 2026 17:57:28 +0200 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20recursive=20api-key=20deny,=20clarify=20SDK=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .claude/settings.json: add Read(**/.api-key.json) so SDK-provisioned key artifacts (packages/sdk/.api-key.json) are denied too, not just the repo root. - CLAUDE.md: note that packages/sdk is linted by ESLint (bun lint in-package), not Biome, so 'bun lint:fix' isn't sufficient there. Addresses Copilot review on #1268. --- .claude/settings.json | 3 ++- CLAUDE.md | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 62ac24011..22b260647 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -9,7 +9,8 @@ "Read(./.env.*)", "Read(**/.env)", "Read(**/.env.*)", - "Read(./.api-key.json)" + "Read(./.api-key.json)", + "Read(**/.api-key.json)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index cbcba020d..f5036edbd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,9 @@ Full wayfinding is in [`MAP.md`](MAP.md). This is a **Bun monorepo** using works ## Monorepo Commands > Always use `bun` — never `npm`, `yarn`, or `pnpm`. Run `bun lint:fix` after any code -> change. Per-app test/dev/migrate commands live in each subdirectory's `CLAUDE.md`. +> change — **except in `packages/sdk`, which is linted by ESLint** (`bun lint` inside that +> package); Biome does not govern it. Per-app test/dev/migrate commands live in each +> subdirectory's `CLAUDE.md`. ```bash bun install # install all dependencies