Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
737b255
feat(gold): import ouro gold app from vortexperiments@010d8e2
ebma Sep 11, 2026
5e5c272
feat(gold): build under /pt-BR/gold with a same-origin api base
ebma Sep 11, 2026
62c3983
feat(frontend): serve gold at /pt-br/gold on the vortex site
ebma Sep 11, 2026
175f15d
docs(gold): add apps/gold to the repository map
ebma Sep 11, 2026
f085a40
docs(frontend): list the gold build variables in netlify.toml
ebma Sep 14, 2026
32d5ba3
feat(gold): drop sms login
ebma Sep 14, 2026
ec25ae6
fix(frontend): build the netlify site when gold or shared inputs change
ebma Sep 14, 2026
09af5e3
feat(gold): add canonical metadata and keep the endorsement in the ap…
ebma Sep 14, 2026
176b6a6
feat(gold): add a copy button for the wallet address in the account menu
ebma Sep 14, 2026
a017b97
feat(gold): serve gold at /gold instead of /pt-br/gold
ebma Sep 14, 2026
6d0e2a3
Merge remote-tracking branch 'origin/staging' into feat/gold-under-vo…
ebma Sep 14, 2026
9362cfb
fix(frontend): type form schemas so they satisfy the standard-schema …
ebma Sep 14, 2026
e49fa83
Merge pull request #1370 from pendulum-chain/feat/gold-under-vortex
ebma Sep 14, 2026
e749755
feat(frontend): alias /pt-BR/gold to the gold app
ebma Sep 15, 2026
452faa0
test(frontend): cover the gold alias in the redirects test
ebma Sep 15, 2026
5ce8ef5
Merge pull request #1374 from pendulum-chain/feat/gold-locale-alias
ebma Sep 15, 2026
707b785
feat(gold): make /pt-br/gold the canonical url
ebma Sep 16, 2026
ea91318
Merge pull request #1376 from pendulum-chain/feat/gold-locale-first
ebma Sep 16, 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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Full wayfinding is in [`MAP.md`](MAP.md). This is a **Bun monorepo** using works

- **apps/frontend** — React 19 + Vite web app → [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md)
- **apps/demo** — minimal browser SDK integration → [`apps/demo/CLAUDE.md`](apps/demo/CLAUDE.md)
- **apps/gold** — "ouro. by Vortex" gold app served at /pt-br/gold → [`apps/gold/CLAUDE.md`](apps/gold/CLAUDE.md)
- **apps/api** — Express backend (PostgreSQL + Sequelize) → [`apps/api/CLAUDE.md`](apps/api/CLAUDE.md)
- **apps/dashboard** — authenticated React dashboard → [`apps/dashboard/CLAUDE.md`](apps/dashboard/CLAUDE.md)
- **apps/rebalancer** — liquidity rebalancing service → [`apps/rebalancer/CLAUDE.md`](apps/rebalancer/CLAUDE.md)
Expand Down
1 change: 1 addition & 0 deletions MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ workspace; use [`docs/README.md`](docs/README.md) to locate durable project cont
| `apps/api` | Express API, PostgreSQL/Sequelize models and migrations, block-flow ramp engine, provider integrations, webhooks, and workers. |
| `apps/frontend` | React widget and public web surface. XState ramp/KYC flows, wallets, and partner embedding. |
| `apps/demo` | Minimal browser-only React example for a BRL/PIX to BSC USDC onramp through `@vortexfi/sdk`. |
| `apps/gold` | "ouro. by Vortex" consumer app for BRL/PIX to PAXG through `@vortexfi/sdk`, served by the frontend site at `/pt-br/gold`. |
| `apps/dashboard` | React account dashboard. Auth, customer entities, onboarding, recipients, history, and self-ramp flows. |
| `apps/rebalancer` | Standalone service for cross-chain liquidity correction and profitability-aware rebalancing. |

Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
# Support the lowercase widget alias without turning unknown localized paths into soft 404s.
/pt-br/widget /_shell.html 200

# Gold's canonical URL is /pt-br/gold/ (plain files under dist/client/pt-BR/gold/, see
# netlify.toml). The unprefixed path redirects until an international Gold landing exists.
/gold /pt-br/gold/ 301
/gold/* /pt-br/gold/:splat 301

# Everything else is a real 404
/* /404.html 404

Expand Down
14 changes: 13 additions & 1 deletion apps/frontend/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@
# Redirect rules live in `_redirects` (copied into dist/client by the build), not here.
# Marketing routes are prerendered to static HTML by TanStack Start, so they are plain files
# under `dist/client`; only the SSR-disabled widget route needs a rewrite.
# Gold (apps/gold) is a separate Vite app built with base /pt-br/gold/ and copied into the
# published tree next to the prerendered locale pages (the pt-BR directory; Netlify matches
# files case-insensitively), so it is served as plain files before any redirect rule applies.
# Gold's build reads these site variables (set in the Netlify UI for all contexts):
# VITE_PRIVY_APP_ID – the Privy app id (without it Gold renders a config error); the
# original gold.satoshipay.io release used cmtiwhqgf01uu0el8lx3ejr35
# VITE_DEMO_MODE – must be "false"; any other value ships the no-money demo
# It shares VITE_SIGNING_SERVICE_PATH with the frontend, so API routing follows the context.
# The dashboard is a separate Netlify site (apps/dashboard/netlify.toml) served on its
# own domain; this build does not include it.

[build]
command = "cd ../.. && bun install --frozen-lockfile && bun run build:shared && bun run build:sdk && bun run build:frontend"
command = "cd ../.. && bun install --frozen-lockfile && bun run build:shared && bun run build:sdk && bun run build:frontend && bun run build:gold && mkdir -p apps/frontend/dist/client/pt-BR/gold && cp -R apps/gold/dist/client/. apps/frontend/dist/client/pt-BR/gold/"
publish = "dist/client"
# Netlify's default ignore check only diffs the base directory, so a commit touching only
# apps/gold or the shared/sdk packages would be skipped as "no content change". Build when
# any input of this site changed (paths are relative to the base directory).
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- . ../gold ../../packages/shared ../../packages/sdk"
2 changes: 1 addition & 1 deletion apps/frontend/src/hooks/ramp/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const createRampFormSchema = (
) => {
return z
.object({
fiatToken: z.string().optional() as z.ZodType<FiatToken | undefined>,
fiatToken: z.string().optional() as z.ZodOptional<z.ZodType<FiatToken>>,
pixId: z.string().optional(),
taxId: z.string().optional(),
walletAddress: z.string().optional()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const FIELD_ATTRS: Record<string, { inputMode?: React.HTMLAttributes<HTMLInputEl
routingNumber: { inputMode: "numeric", maxLength: 9 }
};

function schemaForField(f: FieldDef, accountType: FiatAccountTypeKey, t: TFunction): z.ZodType {
function schemaForField(f: FieldDef, accountType: FiatAccountTypeKey, t: TFunction): z.ZodType<string | undefined> {
if (f.field === "accountAlias") {
return z.string().max(40, t("components.fiatAccountRegistration.validation.nickname")).optional();
}
Expand Down Expand Up @@ -75,7 +75,7 @@ function buildZodSchema(
fields: FieldDef[],
accountType: FiatAccountTypeKey,
t: TFunction
): z.ZodObject<Record<string, z.ZodType>> {
): z.ZodObject<Record<string, z.ZodType<string | undefined>>> {
const shape = Object.fromEntries(fields.map(f => [f.field, schemaForField(f, accountType, t)]));
const base = z.object(shape);

Expand All @@ -92,7 +92,7 @@ function buildZodSchema(
});
}
}
}) as unknown as z.ZodObject<Record<string, z.ZodType>>;
}) as unknown as z.ZodObject<Record<string, z.ZodType<string | undefined>>>;
}

return base;
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/tests/netlify-redirects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ describe("Netlify redirects", () => {
]);
});

it("redirects the unprefixed gold path to its Brazilian edition", () => {
expect(redirects).toContainEqual({ from: "/gold/*", status: "301", to: "/pt-br/gold/:splat" });
});

it("keeps unknown paths on the real 404 fallback", () => {
expect(redirects).toContainEqual({ from: "/*", status: "404", to: "/404.html" });
});
Expand Down
12 changes: 12 additions & 0 deletions apps/gold/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Local visual QA. No real authentication or money movement.
VITE_DEMO_MODE=true

# Public Privy App ID. The Privy App Secret must never be exposed to Vite/browser code.
VITE_PRIVY_APP_ID=cmtiwhqgf01uu0el8lx3ejr35

# Vortex API base: an absolute URL for local development, or a path such as /api/production
# that resolves against the page origin (the Netlify site proxies /api/<env>/* to the API).
# State-changing browser calls use the user's OTP-derived bearer session. pk_* is optional
# for attribution/readiness; sk_* is forbidden here.
VITE_SIGNING_SERVICE_PATH=https://api.vortexfinance.co
VITE_VORTEX_PUBLIC_KEY=
8 changes: 8 additions & 0 deletions apps/gold/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
node_modules
dist/
.env
.env.*
!.env.example
*.log
.DS_Store
24 changes: 24 additions & 0 deletions apps/gold/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# apps/gold - ouro. gold app

Brazil-first consumer app for buying and selling PAX Gold with PIX through `@vortexfi/sdk`,
branded "ouro. by Vortex". Plain JavaScript React 19 + Vite app with Privy embedded wallets;
imported as a snapshot from `pendulum-chain/vortexperiments`, which keeps the pilot history.

- It is served by the frontend Netlify site at `https://www.vortexfinance.co/pt-br/gold/`:
`apps/frontend/netlify.toml` builds it with Vite `base: "/pt-br/gold/"` and copies
`dist/client` into the frontend's `dist/client/pt-BR/gold/`, beside the prerendered locale
pages. The site is locale-first (`/pt-br/business`, `/pt-br/widget`), so future markets go
to `/es-mx/gold/` etc.; the unprefixed `/gold/` 301s to the Brazilian edition until an
international landing exists (`apps/frontend/_redirects`).
Reference public assets through `import.meta.env.BASE_URL`, never as root-relative
`/brand/...` paths.
- The API base comes from `VITE_SIGNING_SERVICE_PATH`, like the frontend: a path such as
`/api/production` resolves against the page origin and goes through the site's `/api/*`
proxy, an absolute URL is used as-is. `VITE_DEMO_MODE` must be `false` for real builds.
- Gold stays on its own tooling: excluded from Biome, tested with `node --test`, no
TypeScript. Do not reformat it to the monorepo style.
- Browser storage keys are namespaced `satoshi:*` so they do not collide with the widget on
the shared origin.

Run `bun dev:gold`, `bun build:gold`, or `cd apps/gold && bun run test` from the repository
root. The SDK must be built first (`bun build:sdk`).
35 changes: 35 additions & 0 deletions apps/gold/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ouro.

Brazil-first consumer pilot for buying and selling PAX Gold with PIX while the user keeps control of an embedded Ethereum wallet.

## What is implemented

- Portuguese landing page and responsive gold dashboard
- Passwordless Privy login with Google first and e-mail under “Outras formas de entrar”, plus automatic embedded Ethereum wallet creation
- Vortex e-mail OTP, refresh-token rotation and concurrent refresh coalescing
- Live Vortex BRL/PIX → PAXG (Ethereum) quote, purchase registration, PIX display, ramp start, status polling and reload recovery
- Brazilian Avenia onboarding: subaccount recovery/creation, ID upload, hosted liveness, KYC submission and provider-status polling
- Explicit fee/slippage disclosure and real on-chain PAXG balance
- PAXG/BRL indicative price chart from CoinGecko; no simulated prices in production
- Encrypted, device-local recovery storage for Vortex ephemeral ramp keys
- Full no-money demo mode for product and mobile QA
- PAXG → BRL/PIX sell flow, explicit ETH-gas funding, wallet confirmations, receipt sequencing and transaction-hash checkpoints for interrupted sessions
- Native Ethereum PAXG availability checked against Vortex's network-specific token list and canonical contract address

## Local run

```bash
cp .env.example .env.local
bun install
bun run dev -- --host 0.0.0.0 --port 4173
```

The checked-in defaults run the safe demo. Use OTP `123456` in demo mode.

## Production configuration

Set `VITE_DEMO_MODE=false` and build with `bun run build`. The browser integration authenticates state-changing Vortex requests with the user's bearer session obtained through e-mail OTP. A `pk_live_*` public value is optional for partner attribution, discount eligibility and public-key-only readiness reads; it is not a substitute for the user's bearer session. Never add the Privy App Secret or a Vortex `sk_*` key to any `VITE_*` variable.

The frontend never receives the Privy App Secret or a Vortex server secret. Vortex session tokens remain in session storage; CPF and identity documents are not persisted by the app.

Live deployment: [https://www.vortexfinance.co/pt-br/gold/](https://www.vortexfinance.co/pt-br/gold/), built and served by the frontend Netlify site (see `apps/frontend/netlify.toml` and `CLAUDE.md` here). The pilot evidence, deployment notes and production checklist from the standalone `gold.satoshipay.io` release stay in `pendulum-chain/vortexperiments`.
16 changes: 16 additions & 0 deletions apps/gold/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#fbf9f5" />
<meta name="description" content="Compre ouro lastreado com PIX e receba direto na sua carteira." />
<link rel="icon" type="image/png" href="/brand/ouro-app-icon.png" />
<link rel="canonical" href="https://www.vortexfinance.co/pt-br/gold/" />
<title>ouro. by Vortex — Compre ouro com PIX</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions apps/gold/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@vortexfi/gold",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "node --test tests/*.test.mjs"
},
"dependencies": {
"@fontsource/cormorant-garamond": "^5.3.0",
"@fontsource/dm-sans": "^5.3.0",
"@phosphor-icons/react": "^2.1.10",
"@privy-io/react-auth": "^3.39.0",
"@vitejs/plugin-react": "^5.0.4",
"@vortexfi/sdk": "workspace:*",
"qrcode.react": "^4.2.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"recharts": "^3.10.1",
"viem": "catalog:",
"vite": "^6.4.2"
}
}
Binary file added apps/gold/public/assets/gold-bar-cutout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/gold/public/assets/gold-bar-ivory.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/gold/public/assets/gold-bar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/gold/public/brand/ouro-app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/gold/public/brand/ouro-wordmark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading