FM Kinetic Wallet — FlowMoney VPN integration
Everything the FM Kinetic Wallet team needs to connect the FM VPN section of the Privacy Center to the FlowMoney VPN Partner API: credential provisioning, authentication, account linking, endpoints, schemas and complete backend code.

Required environment variables
These four names are exact. Set them in the FM Kinetic Wallet project's Secrets interface — never in frontend code, URLs, logs or chat.
| Variable | Value | Example |
|---|---|---|
| FMVPN_PARTNER_ID | The client_id issued at credential provisioning. This is the machine identity of FM Kinetic Wallet. | fmvpn_partner_… |
| FMVPN_PARTNER_API_KEY | The client_secret generated server-side by FlowMoney VPN. Shown exactly once at provisioning or rotation. | 64 hex characters |
| FMVPN_API_BASE_URL | The deployed FlowMoney VPN application URL. Use the current application URL until flowmoneyvpn.com is connected, then the official domain. | https://flowmoneyvpn.com |
| FMVPN_PARTNER_STATUS | The expected authorisation status of the partner, e.g. development. The status endpoint returns the live value to compare against. | development |
Secure credential provisioning
- The FlowMoney VPN administrator opens Administration centre → Partners & API credentials.
- FM Kinetic Wallet (FlowMoney LLC) is already registered and pre-selected. Choose sandbox first, then production after verification.
- Click Generate client credentials. The secret is generated by a cryptographically secure random generator on the server, stored only as a salted hash, and displayed exactly once.
- Copy it immediately and enter it into the wallet project's Secrets interface as FMVPN_PARTNER_API_KEY, with FMVPN_PARTNER_ID as the client_id.
- Never email, chat, log or commit the secret. If it is exposed, rotate it immediately — rotation revokes every previously issued token.
Authentication model
- Server-to-server: the wallet backend exchanges client credentials for a short-lived (1 hour) scoped bearer token.
- Customer linking: single-use, 10-minute link requests with a PKCE S256 challenge; the customer consents on FlowMoney VPN, and the code exchange is bound to that one customer.
- Isolation: a partner can never read another partner's data, and tokens only reach what their scopes allow.
- Never shared: wallet addresses, balances, transaction history, private keys and Privy signing credentials never reach the VPN platform. A financial session alone never authorises VPN access.
API contract and deployed routes
The public contract paths will be served under the official domain once it is connected. Until then the deployed function routes on the right are authoritative, over HTTPS only.
| Contract path | Deployed route | Purpose |
|---|---|---|
| POST /api/v1/partner/token | POST /functions/partnerToken | Client-credential and authorization-code (PKCE) token exchange. |
| GET /api/v1/partner/status | POST /functions/partnerApi { "action": "status" } | Partner status, environment, scopes, rate budget. |
| GET /api/v1/vpn/locations | POST /functions/partnerApi { "action": "locations" } | Published exit locations. |
| GET /api/v1/vpn/servers | POST /functions/partnerApi { "action": "servers" } | Server availability summary, no infrastructure details. |
| GET /api/v1/vpn/availability | POST /functions/partnerApi { "action": "availability" } | Honest availability for a country / city / postal query. |
| POST /api/v1/partner/customer-link | POST /functions/partnerApi { "action": "customer-link" } | Single-use, short-lived link request with PKCE challenge. |
| GET /api/v1/partner/entitlements | POST /functions/partnerApi { "action": "entitlement" } | Consented customer subscription entitlement. |
| POST /api/v1/partner/session | POST /functions/partnerApi { "action": "session" } | Record a session for an entitled, consented customer. |
| POST /api/v1/partner/session/revoke | POST /functions/partnerApi { "action": "session-revoke" } | Revoke the customer’s active session. |
Request and response examples
Flow of a complete integration
# 1. Exchange partner credentials (server-to-server)
POST {FMVPN_API_BASE_URL}/functions/partnerToken
{ "grant_type": "client_credentials",
"client_id": "fmvpn_partner_…", "client_secret": "…" }
200 OK
{ "access_token": "…", "token_type": "Bearer", "expires_in": 3600,
"scopes": ["vpn.locations.read", "vpn.customer.link", "vpn.entitlement.read",
"vpn.connection.read", "vpn.dashboard.open"] }
# 2. Request an account link for the signed-in wallet customer
POST {FMVPN_API_BASE_URL}/functions/partnerApi
Authorization: Bearer <partner token>
{ "action": "customer-link",
"redirect_uri": "https://wallet.flowmoney.app/fm-vpn/callback",
"scopes": "vpn.locations.read vpn.entitlement.read vpn.connection.read vpn.dashboard.open",
"code_challenge": "<S256(verifier)>", "code_challenge_method": "S256" }
200 OK
{ "consent_url": "https://…/authorize?client_id=…&link_state=…",
"link_expires_in": 600, "scopes": ["vpn.locations.read", …] }
# 3. After the customer consents, exchange the single-use code
POST {FMVPN_API_BASE_URL}/functions/partnerToken
{ "grant_type": "authorization_code", "code": "…", "code_verifier": "…",
"redirect_uri": "https://wallet.flowmoney.app/fm-vpn/callback",
"client_id": "fmvpn_partner_…", "client_secret": "…" }
200 OK
{ "access_token": "…", "token_type": "Bearer", "expires_in": 3600,
"scopes": ["vpn.entitlement.read", …], "subject": "customer@example.com" }
# 4. Availability answer when no server exists yet
{ "action": "availability", "country_code": "US", "city": "Chicago" }
200 OK
{ "status": "not_available",
"message": "VPN SERVER NOT YET AVAILABLE IN THIS LOCATION",
"locations": [], "servers": [], "infrastructure_online": false,
"note": "Availability reflects the operator-confirmed registry only. A postal
code never guarantees an exit IP within that postal code." }Complete compatible backend function
Copy this file into the FM Kinetic Wallet Base44 project as base44/functions/fmVpnPartner/entry.ts. It keeps every FlowMoney VPN credential server-side and is the single integration point for the wallet.
// base44/functions/fmVpnPartner/entry.ts — FM KINETIC WALLET project
// All FlowMoney VPN credentials stay in this project's Secrets interface.
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54';
import { secrets } from 'base44:runtime';
const apiBase = () => (secrets.get('FMVPN_API_BASE_URL') || '').replace(/\/$/, '');
const partnerId = () => secrets.get('FMVPN_PARTNER_ID') || '';
const partnerKey = () => secrets.get('FMVPN_PARTNER_API_KEY') || '';
const expectedStatus = () => secrets.get('FMVPN_PARTNER_STATUS') || 'development';
const CUSTOMER_ACTIONS = ['locations', 'availability', 'entitlement', 'connection', 'dashboard-url'];
let cachedPartnerToken = { value: '', expiresAt: 0 };
const customerTokens = new Map();
const linkRequests = new Map();
async function partnerToken() {
if (cachedPartnerToken.value && Date.now() < cachedPartnerToken.expiresAt) return cachedPartnerToken.value;
const response = await fetch(apiBase() + '/functions/partnerToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'client_credentials', client_id: partnerId(), client_secret: partnerKey() }),
signal: AbortSignal.timeout(15000),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.message || 'FlowMoney VPN authentication failed');
cachedPartnerToken = { value: data.access_token, expiresAt: Date.now() + (data.expires_in - 60) * 1000 };
return cachedPartnerToken.value;
}
async function callPartnerApi(payload, token) {
const bearer = token || await partnerToken();
const response = await fetch(apiBase() + '/functions/partnerApi', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + bearer },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15000),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.message || data.error || 'Partner API error ' + response.status);
return data;
}
const b64Url = (bytes) => {
let out = '';
new Uint8Array(bytes).forEach((b) => { out += String.fromCharCode(b); });
return btoa(out).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
};
const randomVerifier = () => { const b = new Uint8Array(32); crypto.getRandomValues(b); return b64Url(b); };
const challengeFrom = async (verifier) => {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
return b64Url(digest);
};
export default async function (req) {
try {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 });
const body = await req.json().catch(() => ({}));
const action = body?.action || 'status';
if (action === 'status') {
const data = await callPartnerApi({ action: 'status' });
return Response.json({
partner_status: data.partner_status,
expected_status: expectedStatus(),
status_matches: data.partner_status === expectedStatus(),
environment: data.environment,
scopes: data.scopes,
});
}
if (action === 'start-link') {
const codeVerifier = randomVerifier();
const codeChallenge = await challengeFrom(codeVerifier);
const data = await callPartnerApi({
action: 'customer-link',
redirect_uri: 'https://wallet.flowmoney.app/fm-vpn/callback',
scopes: 'vpn.locations.read vpn.entitlement.read vpn.connection.read vpn.dashboard.open',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
linkRequests.set(user.id, { codeVerifier, expiresAt: Date.now() + 9 * 60 * 1000 });
return Response.json({ consent_url: data.consent_url, expires_in: data.link_expires_in });
}
if (action === 'complete-link') {
const stored = linkRequests.get(user.id);
linkRequests.delete(user.id);
if (!stored || Date.now() > stored.expiresAt) {
return Response.json({ error: 'link_expired', message: 'Start the link again.' }, { status: 400 });
}
const response = await fetch(apiBase() + '/functions/partnerToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: body?.code,
code_verifier: stored.codeVerifier,
redirect_uri: 'https://wallet.flowmoney.app/fm-vpn/callback',
client_id: partnerId(),
client_secret: partnerKey(),
}),
signal: AbortSignal.timeout(15000),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
return Response.json({ error: data.error || 'link_failed', message: data.message }, { status: 400 });
}
customerTokens.set(user.id, { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 });
return Response.json({ linked: true, subject: data.subject, scopes: data.scopes, expires_in: data.expires_in });
}
if (!CUSTOMER_ACTIONS.includes(action)) {
return Response.json({ error: 'unknown_action' }, { status: 400 });
}
const customer = customerTokens.get(user.id);
if (!customer || Date.now() > customer.expiresAt) {
return Response.json({ error: 'not_linked', message: 'Link your FlowMoney VPN account first.' }, { status: 403 });
}
const data = await callPartnerApi({ action, access_token: customer.token });
return Response.json(data);
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}Privacy Center FM VPN section
The FM VPN panel opens from the Privacy Center, links the customer's identity with explicit consent, then shows entitlement, locations and their FlowMoney VPN dashboard.
// FM VPN section inside the wallet's Privacy Center (React)
import { base44 } from '@/api/base44Client';
// 1. Show partner status in the Privacy Center
const [fmvpn, setFmvpn] = useState({ status: 'loading' });
useEffect(() => {
base44.functions.invoke('fmVpnPartner', { action: 'status' })
.then((res) => setFmvpn({ status: 'ready', ...res.data }))
.catch(() => setFmvpn({ status: 'offline' }));
}, []);
// 2. "Connect FM VPN" button → customer consents on FlowMoney VPN
const openFmVpn = async () => {
const res = await base44.functions.invoke('fmVpnPartner', { action: 'start-link' });
window.open(res.data.consent_url, '_blank', 'noopener');
};
// 3. Callback page at /fm-vpn/callback (FlowMoney VPN redirects here with ?code=…)
const completeLink = async () => {
const params = new URLSearchParams(window.location.search);
if (params.get('error')) { /* customer declined: show a neutral message */ return; }
const res = await base44.functions.invoke('fmVpnPartner', {
action: 'complete-link',
code: params.get('code'),
});
if (res.data.linked) {
// 4. Linked: show entitlement, locations and the dashboard link
const entitlement = await base44.functions.invoke('fmVpnPartner', { action: 'entitlement' });
const locations = await base44.functions.invoke('fmVpnPartner', { action: 'locations' });
const dashboard = await base44.functions.invoke('fmVpnPartner', { action: 'dashboard-url' });
window.open(dashboard.data.dashboard_url, '_blank', 'noopener');
}
};
// The native client launch button appears when the platform reports a
// deployable client — do not promise tunnel connectivity before then.Connection testing procedure
- Invoke fmVpnPartner { action: "status" } — expect the partner name, environment and status_matches: true.
- Call start-link — expect a consent URL with a 10-minute expiry.
- Open the consent URL, sign in to FlowMoney VPN, approve — the browser returns to the callback with a single-use code.
- Call complete-link — expect linked: true and the consented scopes.
- Replay the same code — expect rejection (single-use enforcement).
- Call entitlement and locations — expect an honest entitlement answer and the current availability registry.
- Try an action without linking — expect not_linked. Try a bad API key — expect authentication failure.
- Verify the panel on mobile: the same flows must complete at phone width.
Credential rotation
- Ask the FlowMoney VPN administrator to rotate the credential in the Administration centre.
- A new secret is generated and shown once; every previously issued token is revoked immediately.
- Enter the new secret into the wallet's Secrets interface, replacing FMVPN_PARTNER_API_KEY.
- Rotation is due every 90 days, or immediately on any suspicion of exposure.
- Revoke instead of rotate if the partnership is suspended — the credential and all its tokens die at once.
This package contains no secret values. Credentials are provisioned by the FlowMoney VPN administrator and transferred through the wallet project's own Secrets interface. Encrypted VPN connectivity itself activates when FlowMoney VPN's WireGuard infrastructure and native clients are deployed.
