Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c6e8460
feat: add fiat and crypto subscription payment components
Jul 29, 2026
207f4a1
feat: switch fiat onramp from Privy/Stripe to Transak (native VET sup…
Jul 30, 2026
417b090
fix: keep Transak modal open on success + remove dead code in subscribe
Jul 30, 2026
2d14fef
fix: auto-hide 'Buy' quick action when transak.apiKey is not configured
Jul 30, 2026
f1e70a6
feat: gate 'Pay with Card' on transak.apiKey presence
Jul 30, 2026
8709dcb
feat: require Transak Secure Widget URL flow
Aug 3, 2026
95a9fb1
fix: embed Transak widget in host modal to stop focus flicker
Aug 3, 2026
e48e4c2
fix: lower demo amount to Transak minimum and fix widget scrollbars
Aug 3, 2026
b274388
fix: lower default Transak amount to $10 minimum
Aug 3, 2026
69e57b3
fix: theme standalone Transak/subscription modals and fix cross-app-c…
Aug 3, 2026
536f54f
feat: auto-derive Transak environment from connected network
Aug 3, 2026
4c7d9a9
docs: add Transak on-ramp backend setup guide
Aug 3, 2026
4cefe6b
chore: remove half-implemented subscription flow, revert Privy to v2,…
Aug 3, 2026
e339813
fix: address CodeRabbit findings on Transak checkout and reference route
Aug 3, 2026
bc356b4
feat(next-template): allow overriding the Transak mini-server URL via…
Aug 4, 2026
8819a59
feat(homepage): wire Transak onramp + pass env vars to preview deploy
Aug 4, 2026
96468ca
chore: trigger preview redeploy with Transak env vars
Aug 4, 2026
3e752da
feat: wire Transak environment + add mandatory integration requiremen…
Aug 5, 2026
adf2410
ci: split Transak API key secret per environment
Aug 5, 2026
580e744
Merge branch 'main' into feat/transak-onramp
Aug 5, 2026
60cfbb9
fix: harden rate-limit IP resolution + guard stale generation in tran…
Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy-cloudfront.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:
NEXT_PUBLIC_PRIVY_APP_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}
NEXT_PUBLIC_PRIVY_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_CLIENT_ID }}
NEXT_PUBLIC_DELEGATOR_URL: ${{ secrets.NEXT_PUBLIC_DELEGATOR_URL }}
NEXT_PUBLIC_TRANSAK_API_KEY: ${{ secrets.NEXT_PUBLIC_TRANSAK_API_KEY_PROD }}
Comment thread
Vombato marked this conversation as resolved.
NEXT_PUBLIC_TRANSAK_API_URL: ${{ secrets.NEXT_PUBLIC_TRANSAK_API_URL }}
Comment thread
Vombato marked this conversation as resolved.
NEXT_PUBLIC_TRANSAK_ENVIRONMENT: 'production'
NEXT_PUBLIC_NETWORK_TYPE: 'main'
AWS_REGION: eu-west-1
steps:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/deploy-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ jobs:
NEXT_PUBLIC_PRIVY_APP_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}
NEXT_PUBLIC_PRIVY_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_CLIENT_ID }}
NEXT_PUBLIC_DELEGATOR_URL: ${{ secrets.NEXT_PUBLIC_DELEGATOR_URL }}
NEXT_PUBLIC_TRANSAK_API_KEY: ${{ secrets.NEXT_PUBLIC_TRANSAK_API_KEY }}
NEXT_PUBLIC_TRANSAK_API_KEY: ${{ secrets.NEXT_PUBLIC_TRANSAK_API_KEY_STAGING }}
Comment thread
Vombato marked this conversation as resolved.
NEXT_PUBLIC_TRANSAK_API_URL: ${{ secrets.NEXT_PUBLIC_TRANSAK_API_URL }}
NEXT_PUBLIC_TRANSAK_ENVIRONMENT: 'staging'
NEXT_PUBLIC_NETWORK_TYPE: 'main'
AWS_REGION: eu-west-1
BRANCH_NAME: ${{ github.event.pull_request.head.ref }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/lint-build-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ jobs:
- name: Install
run: yarn && yarn install:all

- name: Typecheck
run: yarn kit:typecheck

- name: Build
run: yarn build

Expand Down
218 changes: 218 additions & 0 deletions docs/recipes/transak-onramp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Transak On-Ramp Backend API

This document describes the backend required by vechain-kit's fiat on-ramp feature (`PayWithTransakButton`, `TransakCheckoutModal`).

## Why a backend is required

Transak deprecated direct widget URLs (API key in the query string). Widgets now only load from a **Secure Widget URL** minted by Transak's Create Widget URL API, which is authenticated with your **API secret**. That secret must never be exposed in the browser, so a small server endpoint is required.

The flow is:

```text
Browser (vechain-kit) Your backend Transak
┌──────────────────┐ POST ┌──────────────────┐ POST ┌──────────────────┐
│ widgetUrlBuilder │ ───────► │ /api/transak/ │ ──────► │ /api/v2/ │
│ (kit passes │ │ widget-url │ ①token │ auth/session │
│ walletAddress, │ │ │ │ │
│ fiat, env...) │ │ ② POST widget │ ◄────── │ returns │
│ │ │ (server-to- │ widget │ secure widget │
│ │ ◄─────── │ server) │ URL │ URL (5 min TTL) │
└──────────────────┘ widget └──────────────────┘ └──────────────────┘
URL returned ▲
└─► Transak SDK loads │ ① access token via POST /partners/api/v2/refresh-token
│ (7-day validity, cache it)
```

## 1. Create Transak API credentials

1. Go to the [Transak Partner Dashboard](https://dashboard.transak.com) and create a partner account.
2. Generate an **API key** and **API secret**. Credentials are environment-specific:
- **Sandbox** credentials (for testing) — work against the `staging` endpoints.
- **Production** credentials — work against the `production` endpoints.
3. Add the `referrerDomain` of your app (the domain the widget will be served from) as an allowed referrer, otherwise session creation fails with `403`.
4. Transak requires the end user's real IP on its authenticated APIs (`x-user-ip` header) — your server must forward the caller's IP.

## 2. Server and client environment variables

| Variable | Scope | Description |
| --- | --- | --- |
| `TRANSAK_API_KEY` | Server only | Partner API key. Never in the browser bundle. |
| `TRANSAK_API_SECRET` | Server only | Partner API secret. Keep it on the backend only. |
| `NEXT_PUBLIC_TRANSAK_API_KEY` | Client | Same API key, safe to expose (works only with the server-side secret). |
| `NEXT_PUBLIC_TRANSAK_API_URL` | Client | Base URL of your mini-server (leave empty for same-origin). |
| `NEXT_PUBLIC_TRANSAK_ENVIRONMENT` | Client | `'staging'` or `'production'`. Overrides the kit's auto-derived value. Set to `'staging'` for preview deployments and `'production'` for live. |

## 3. Endpoint contract

### `POST /api/transak/widget-url`

**Request:**
```json
{
"environment": "staging",
"widgetParams": {
"apiKey": "your_transak_api_key",
"referrerDomain": "https://yourapp.com",
"walletAddress": "0x...",
"fiatAmount": "10",
"fiatCurrency": "USD",
"cryptoCurrencyCode": "VET",
"network": "vechain",
"defaultCryptoCurrency": "VET",
"disableWalletAddressForm": true
}
}
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

| Field | Description |
| --- | --- |
| `environment` | Which Transak environment to use. vechain-kit derives it automatically from the connected VeChain network (`main` → `production`, `test`/`solo` → `staging`) unless overridden via `TransakConfig.environment` on `VeChainKitProvider`. Your endpoint should trust this value and select the matching Transak base URLs. |
| `widgetParams` | Transak widget configuration, see the [Transak query parameters docs](https://docs.transak.com/customization/query-parameters). |

**Server behavior:**

1. Obtain a partner access token via `POST https://api-stg.transak.com/partners/api/v2/refresh-token` (staging) or `POST https://api.transak.com/partners/api/v2/refresh-token` (production), with headers `x-api-key` and `api-secret` and body `{ "apiKey": ... }`. The token is valid for **7 days** — cache it and reuse it. Cache per environment; a staging token must never be sent to production endpoints.
2. Mint the widget session via `POST https://api-gateway-stg.transak.com/api/v2/auth/session` (staging) or `POST https://api-gateway.transak.com/api/v2/auth/session` (production), with headers `x-api-key`, `access-token` and `x-user-ip` (the caller's real IP, e.g. from `x-forwarded-for`), and body `{ "widgetParams": ... }`.
3. Return Transak's response.

**Response (200):**
```json
{
"data": {
"widgetUrl": "https://global-stg.transak.com/...?sessionId=..."
}
}
```

The widget URL is valid for **5 minutes** and single-use — mint a fresh one per checkout.

## 4. Wire it into vechain-kit

```tsx
<VeChainKitProvider
transak={{
apiKey: process.env.NEXT_PUBLIC_TRANSAK_API_KEY,
widgetUrlBuilder: async ({
walletAddress,
fiatAmount,
fiatCurrency,
cryptoCurrency,
network,
}) => {
const res = await fetch('/api/transak/widget-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
environment: process.env.NEXT_PUBLIC_TRANSAK_ENVIRONMENT as
| 'staging'
| 'production'
| undefined,
widgetParams: {
apiKey: process.env.NEXT_PUBLIC_TRANSAK_API_KEY,
referrerDomain: window.location.origin,
walletAddress,
fiatAmount,
fiatCurrency,
cryptoCurrencyCode: cryptoCurrency,
network,
defaultCryptoCurrency: 'VET',
disableWalletAddressForm: true,
},
}),
});
const json = await res.json();
if (!res.ok) throw new Error(json?.error ?? 'Failed to create Transak widget URL');
return json.data.widgetUrl;
},
}}
>
```

The `environment` parameter is passed through to your `widgetUrlBuilder` — your backend uses it to select the correct Transak endpoints. Set `transak.environment` explicitly (e.g. `NEXT_PUBLIC_TRANSAK_ENVIRONMENT='staging'` for previews, `'production'` for live) when you want to force the value regardless of the connected VeChain network. Omit it to let the kit auto-derive (`main` → `production`, `test`/`solo` → `staging`).

## 5. Reference implementation

A complete, working Next.js implementation ships in the repo:

- Route: `examples/next-template/src/app/api/transak/widget-url/route.ts`
- Provider wiring: `examples/next-template/src/app/providers/VechainKitProviderWrapper.tsx`
- Env vars: `examples/next-template/.env.example`

## 6. Mandatory Transak integration requirements

These are **architectural prerequisites** for any production deployment of the on-ramp feature, regardless of the backend framework you choose. Transak enforces them server-side; missing any of them results in `403` / `401` errors from the Transak API.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### 6.1 Credential isolation

| Property | Value | Where |
| --- | --- | --- |
| `TRANSAK_API_KEY` | Partner API key | Server env only — never in the browser bundle |
| `TRANSAK_API_SECRET` | Partner API secret | Server env only — never sent to the client under any circumstance |
| `NEXT_PUBLIC_TRANSAK_API_KEY` | Same API key, safe to expose | Client bundle (used by the kit to pass through to your backend) |

The **API key** is safe to ship in the client bundle — it only works together with the **API secret**, which must stay server-side. The kit never needs the secret.

### 6.2 `x-api-key` header (server-to-Transak only)

Transak requires `x-api-key` on every authenticated API call (`refresh-token` and `auth/session`). Set it from `TRANSAK_API_KEY` **on the backend**, never from a client-supplied value. The `apiKey` in `widgetParams` is the partner-config value for the widget session, not the authentication key — do not conflate them.

### 6.3 `x-user-ip` header (forward the real caller IP)

Transak needs the end user's real IP for fraud detection and geo-compliance. Your backend **must** forward it as `x-user-ip` on the `auth/session` call. Use the leftmost public IP from:

```
x-forwarded-for: 203.0.113.42, 10.0.0.1
```

Do not send your server's own IP — Transak will reject shared/NAT IPs in production. If you run behind a CDN or proxy, ensure the chain preserves the original client IP (e.g. CloudFront → `X-Forwarded-For`, ALB → `X-Forwarded-For`).

### 6.4 Backend IP whitelisting

Transak's production API can be locked to a set of **whitelisted source IPs** (configured in the Partner Dashboard under *API Settings → IP Whitelisting*). Your backend must have a **fixed egress IP** (or a small known range). This is mandatory for production — staging does not enforce it.

Checklist:

- [ ] Deploy the backend on infrastructure with a static NAT / elastic IP (Lambda behind API Gateway, a fixed-IP container, or a VPS with a static IP)
- [ ] Add every backend egress IP to the Transak IP whitelist in the Partner Dashboard
- [ ] If you use a serverless platform with rotating IPs (e.g. Vercel functions without a NAT gateway), put a proxy with a fixed IP in front, or move the endpoint to infra that provides one

### 6.5 CORS — restrict to your domains only

Your `/api/transak/widget-url` endpoint must set CORS headers that allow **only your own origins**:

```http
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
```

Never use `Access-Control-Allow-Origin: *` — any website could then mint Transak sessions on your billing account. For multiple environments, maintain an explicit allow-list:

```js
const ALLOWED_ORIGINS = new Set([
'https://yourapp.com',
'https://preview.vechainkit.vechain.org',
]);
const origin = req.headers.get('origin');
if (ALLOWED_ORIGINS.has(origin)) {
res.headers.set('Access-Control-Allow-Origin', origin);
}
```

The `referrerDomain` sent in `widgetParams` must also match one of the domains registered in your Transak Partner Dashboard (see step 3 in section 1 above). Transak checks it server-side; a mismatch yields `403`.

### 6.6 Rate limiting

Your endpoint is unauthenticated from the client's perspective — protect it with an in-memory or distributed rate limit so a single caller cannot mint unlimited Transak sessions against your credentials. The reference implementation in the repo uses a simple in-memory cap (20 requests/minute/IP). For horizontally-scaled backends, use a shared store (Redis, DynamoDB, etc.).

### 6.7 Partner onboarding checklist

Before going live, confirm every item in the Transak Partner Dashboard:

- [ ] Production API key + secret generated (not the sandbox ones)
- [ ] All frontend domains added to **Allowed Referrer Domains** (production + any preview subdomains)
- [ ] All backend egress IPs added to **IP Whitelisting** (production only)
- [ ] KYC/AML settings configured (required countries, default fiat currency, default crypto)
- [ ] Wallet address validation enabled (`disableWalletAddressForm: true` — the kit passes the connected wallet address directly; do not let users type an arbitrary address)
- [ ] Webhook URL configured (for order status callbacks — not handled by the kit, must be on your backend)
- [ ] Test purchase completed end-to-end on staging before switching to production credentials
4 changes: 4 additions & 0 deletions examples/homepage/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
NEXT_PUBLIC_DELEGATOR_URL=
# Network Configuration -- main | test | solo
NEXT_PUBLIC_NETWORK_TYPE=
# Transak onramp -- staging key from dashboard.transak.com (leave empty to hide the Buy button)
NEXT_PUBLIC_TRANSAK_API_KEY=
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Mini-server base URL that mints the Secure Widget URL (leave empty to use the same origin)
NEXT_PUBLIC_TRANSAK_API_URL=

#For translations
OPENAI_API_KEY=
56 changes: 56 additions & 0 deletions examples/homepage/src/app/providers/VechainKitProviderWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,62 @@ export function VechainKitProviderWrapper({ children }: Props) {
// nodeUrl: 'http://localhost:8669',
}}
allowCustomTokens={true}
transak={
process.env.NEXT_PUBLIC_TRANSAK_API_KEY
? {
apiKey: process.env.NEXT_PUBLIC_TRANSAK_API_KEY,
widgetUrlBuilder: async ({
walletAddress,
fiatAmount,
fiatCurrency,
cryptoCurrency,
network,
}) => {
const apiUrl =
process.env.NEXT_PUBLIC_TRANSAK_API_URL ?? '';
const res = await fetch(
`${apiUrl}/api/transak/widget-url`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
environment:
process.env
.NEXT_PUBLIC_TRANSAK_ENVIRONMENT as
| 'staging'
| 'production'
| undefined,
widgetParams: {
apiKey: process.env.NEXT_PUBLIC_TRANSAK_API_KEY,
referrerDomain:
typeof window !== 'undefined'
? window.location.origin
: '',
walletAddress,
fiatAmount,
fiatCurrency,
cryptoCurrencyCode: cryptoCurrency,
network,
defaultCryptoCurrency: 'VET',
disableWalletAddressForm: true,
},
}),
},
);
const json = await res.json();
if (!res.ok) {
throw new Error(
json?.error ??
'Failed to create Transak widget URL',
);
}
return json.data.widgetUrl as string;
},
}
: undefined
}
>
<LanguageSync>{children}</LanguageSync>
</VeChainKitProvider>
Expand Down
10 changes: 9 additions & 1 deletion examples/next-template/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ NEXT_PUBLIC_PRIVY_CLIENT_ID=
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
NEXT_PUBLIC_DELEGATOR_URL=
# Network Configuration -- main | test | solo
NEXT_PUBLIC_NETWORK_TYPE=
NEXT_PUBLIC_NETWORK_TYPE=
# Transak onramp -- staging key from dashboard.transak.com (leave empty to disable)
NEXT_PUBLIC_TRANSAK_API_KEY=
# Mini-server base URL -- where /api/transak/widget-url lives. Leave empty to
# use the same origin (e.g. https://my-preview.example.com in previews).
NEXT_PUBLIC_TRANSAK_API_URL=
# Transak backend credentials -- used by /api/transak/widget-url to mint a Secure Widget URL
TRANSAK_API_KEY=
TRANSAK_API_SECRET=
Loading
Loading