diff --git a/og-service/Makefile b/og-service/Makefile
new file mode 100644
index 0000000..096beb6
--- /dev/null
+++ b/og-service/Makefile
@@ -0,0 +1,18 @@
+APP_NAME=portfoliojournal-og
+BUILD_DIR=bin
+PORT?=8090
+
+.PHONY: build run clean install-service
+
+build:
+ @mkdir -p $(BUILD_DIR)
+ GO111MODULE=on go build -o $(BUILD_DIR)/$(APP_NAME) .
+
+run:
+ PORT=$(PORT) GO111MODULE=on go run .
+
+clean:
+ @rm -rf $(BUILD_DIR)
+
+install-service: build
+ @echo "Install the binary and OpenRC service using the instructions in README.md"
diff --git a/og-service/README.md b/og-service/README.md
new file mode 100644
index 0000000..2443e74
--- /dev/null
+++ b/og-service/README.md
@@ -0,0 +1,81 @@
+# OG Share Service
+
+This service powers dynamic Open Graph share pages and images for Portfolio Journal.
+
+## Endpoints
+
+- `/share?data=BASE64URL` renders the HTML page with OG tags.
+- `/og?data=BASE64URL` renders the 1200x630 PNG image.
+
+Payload (JSON, base64url-encoded):
+
+```json
+{
+ "n": "Goal name",
+ "r": 12500.5,
+ "g": 20000,
+ "c": "USD",
+ "td": "2026-12-31",
+ "ec": "2026-10-15",
+ "p": false
+}
+```
+
+Fields:
+- `n` goal name
+- `r` current amount
+- `g` goal amount
+- `c` currency code (ISO 4217)
+- `td` target date (YYYY-MM-DD, optional)
+- `ec` estimated completion date (YYYY-MM-DD, optional)
+- `p` privacy mode; when true, hides the current amount
+
+## Run locally
+
+```bash
+cd og-service
+GO111MODULE=on go run .
+```
+
+## OpenRC (Alpine) service
+
+Example steps (requires root):
+
+```bash
+# Build and install the binary
+make build
+cp og-service/bin/portfoliojournal-og /usr/local/bin/portfoliojournal-og
+chmod +x /usr/local/bin/portfoliojournal-og
+
+# Install the OpenRC script
+cp og-service/portfoliojournal-og.openrc /etc/init.d/portfoliojournal-og
+chmod +x /etc/init.d/portfoliojournal-og
+
+# Enable and start
+rc-update add portfoliojournal-og default
+rc-service portfoliojournal-og start
+```
+
+Or run the helper script (requires root):
+
+```bash
+sudo sh og-service/install-openrc.sh
+```
+
+## NGINX example
+
+```nginx
+location /share {
+ proxy_pass http://127.0.0.1:8090;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+}
+
+location /og {
+ proxy_pass http://127.0.0.1:8090;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+}
+```
diff --git a/og-service/go.mod b/og-service/go.mod
new file mode 100644
index 0000000..7888daf
--- /dev/null
+++ b/og-service/go.mod
@@ -0,0 +1,9 @@
+module portfoliojournal.app/ogservice
+
+go 1.22
+
+require (
+ github.com/fogleman/gg v1.3.0
+ golang.org/x/image v0.21.0
+ golang.org/x/text v0.17.0
+)
diff --git a/og-service/install-openrc.sh b/og-service/install-openrc.sh
new file mode 100644
index 0000000..5cf1564
--- /dev/null
+++ b/og-service/install-openrc.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+set -e
+
+APP_NAME="portfoliojournal-og"
+SERVICE_FILE="/etc/init.d/${APP_NAME}"
+BIN_TARGET="/usr/local/bin/${APP_NAME}"
+
+echo "Building ${APP_NAME}..."
+make -C "$(dirname "$0")" build
+
+echo "Installing binary to ${BIN_TARGET}..."
+cp "$(dirname "$0")/bin/${APP_NAME}" "${BIN_TARGET}"
+chmod +x "${BIN_TARGET}"
+
+echo "Installing OpenRC service to ${SERVICE_FILE}..."
+cp "$(dirname "$0")/${APP_NAME}.openrc" "${SERVICE_FILE}"
+chmod +x "${SERVICE_FILE}"
+
+echo "Enabling service..."
+rc-update add "${APP_NAME}" default
+
+echo "Starting service..."
+rc-service "${APP_NAME}" start
+
+echo "Done."
diff --git a/og-service/main.go b/og-service/main.go
new file mode 100644
index 0000000..91b8af3
--- /dev/null
+++ b/og-service/main.go
@@ -0,0 +1,626 @@
+package main
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "image/color"
+ "log"
+ "math"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/fogleman/gg"
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/gobold"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/opentype"
+ "golang.org/x/text/currency"
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
+)
+
+const (
+ imageWidth = 1200
+ imageHeight = 630
+)
+
+type shareData struct {
+ Name string `json:"n"`
+ RealAmount json.Number `json:"r"`
+ GoalAmount json.Number `json:"g"`
+ Currency string `json:"c"`
+ TargetDate string `json:"td"`
+ EstimateCompletion string `json:"ec"`
+ PrivateMode bool `json:"p"`
+}
+
+type shareView struct {
+ Title string
+ Description string
+ OGImageURL string
+ ShareURL string
+ GoalName string
+ CurrentAmount string
+ TargetAmount string
+ PercentText string
+ ProgressPercent float64
+ TargetDateLabel string
+ EstimateDateLabel string
+ PrivateMode bool
+ FormattedGoalAmount string
+}
+
+func main() {
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "8090"
+ }
+
+ http.HandleFunc("/share", shareHandler)
+ http.HandleFunc("/og", ogHandler)
+
+ log.Printf("og-service listening on :%s", port)
+ log.Fatal(http.ListenAndServe(":"+port, nil))
+}
+
+func shareHandler(w http.ResponseWriter, r *http.Request) {
+ dataParam := r.URL.Query().Get("data")
+ if dataParam == "" {
+ http.Error(w, "missing data parameter", http.StatusBadRequest)
+ return
+ }
+
+ payload, err := decodeData(dataParam)
+ if err != nil {
+ http.Error(w, "invalid data parameter", http.StatusBadRequest)
+ return
+ }
+
+ view := buildShareView(payload, r, dataParam)
+ html := renderShareHTML(view)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "no-store")
+ _, _ = w.Write([]byte(html))
+}
+
+func ogHandler(w http.ResponseWriter, r *http.Request) {
+ dataParam := r.URL.Query().Get("data")
+ if dataParam == "" {
+ http.Error(w, "missing data parameter", http.StatusBadRequest)
+ return
+ }
+
+ payload, err := decodeData(dataParam)
+ if err != nil {
+ http.Error(w, "invalid data parameter", http.StatusBadRequest)
+ return
+ }
+
+ pngBytes, err := renderOGImage(payload)
+ if err != nil {
+ http.Error(w, "failed to render image", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "image/png")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ _, _ = w.Write(pngBytes)
+}
+
+func decodeData(param string) (shareData, error) {
+ var data shareData
+
+ decoded, err := base64.RawURLEncoding.DecodeString(param)
+ if err != nil {
+ decoded, err = base64.URLEncoding.DecodeString(param)
+ if err != nil {
+ return data, err
+ }
+ }
+
+ dec := json.NewDecoder(bytes.NewReader(decoded))
+ dec.UseNumber()
+ if err := dec.Decode(&data); err != nil {
+ return data, err
+ }
+
+ data.Name = strings.TrimSpace(data.Name)
+ data.Currency = strings.ToUpper(strings.TrimSpace(data.Currency))
+ if data.Currency == "" {
+ data.Currency = "USD"
+ }
+
+ return data, nil
+}
+
+func buildShareView(data shareData, r *http.Request, rawParam string) shareView {
+ goalAmount, _ := numberToFloat(data.GoalAmount)
+ realAmount, _ := numberToFloat(data.RealAmount)
+
+ percent := 0.0
+ if goalAmount > 0 {
+ percent = (realAmount / goalAmount) * 100
+ }
+
+ percentText := "Progress unavailable"
+ if goalAmount > 0 {
+ percentText = fmt.Sprintf("%.0f%% complete", percent)
+ }
+
+ currencyTag := localeForCurrency(data.Currency)
+ formattedGoal := formatCurrency(goalAmount, data.Currency, currencyTag)
+ formattedReal := formatCurrency(realAmount, data.Currency, currencyTag)
+
+ currentAmount := formattedReal
+ if data.PrivateMode {
+ currentAmount = "Hidden"
+ }
+
+ goalName := data.Name
+ if goalName == "" {
+ goalName = "Investment Goal"
+ }
+
+ description := fmt.Sprintf("%s toward %s", percentText, formattedGoal)
+ if data.PrivateMode {
+ description = fmt.Sprintf("Goal progress: %s", percentText)
+ }
+
+ baseURL := buildBaseURL(r)
+ shareURL := fmt.Sprintf("%s/share?data=%s", baseURL, url.QueryEscape(rawParam))
+ ogImageURL := fmt.Sprintf("%s/og?data=%s", baseURL, url.QueryEscape(rawParam))
+
+ return shareView{
+ Title: fmt.Sprintf("Portfolio Journal — %s", goalName),
+ Description: description,
+ OGImageURL: ogImageURL,
+ ShareURL: shareURL,
+ GoalName: goalName,
+ CurrentAmount: currentAmount,
+ TargetAmount: formattedGoal,
+ PercentText: percentText,
+ ProgressPercent: percent,
+ TargetDateLabel: formatDateLabel("Target", data.TargetDate),
+ EstimateDateLabel: formatDateLabel("Est. completion", data.EstimateCompletion),
+ PrivateMode: data.PrivateMode,
+ FormattedGoalAmount: formattedGoal,
+ }
+}
+
+func renderShareHTML(view shareView) string {
+ htmlTemplate := `
+
+
+
+
+ ` + escapeHTML(view.Title) + `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Portfolio Journal
+
` + escapeHTML(view.GoalName) + `
+
+
+ Current amount
+ ` + escapeHTML(view.CurrentAmount) + `
+
+
+ Goal amount
+ ` + escapeHTML(view.TargetAmount) + `
+
+
+
` + escapeHTML(view.PercentText) + `
+
+
` + escapeHTML(joinLabels(view.TargetDateLabel, view.EstimateDateLabel)) + `
+
Download on the App Store
+
+
+`
+
+ return htmlTemplate
+}
+
+func renderOGImage(data shareData) ([]byte, error) {
+ goalAmount, _ := numberToFloat(data.GoalAmount)
+ realAmount, _ := numberToFloat(data.RealAmount)
+ percent := 0.0
+ if goalAmount > 0 {
+ percent = (realAmount / goalAmount) * 100
+ }
+
+ currencyTag := localeForCurrency(data.Currency)
+ formattedGoal := formatCurrency(goalAmount, data.Currency, currencyTag)
+ formattedReal := formatCurrency(realAmount, data.Currency, currencyTag)
+ if data.PrivateMode {
+ formattedReal = "Hidden"
+ }
+
+ goalName := data.Name
+ if goalName == "" {
+ goalName = "Investment Goal"
+ }
+
+ dc := gg.NewContext(imageWidth, imageHeight)
+
+ grad := gg.NewLinearGradient(0, 0, imageWidth, imageHeight)
+ grad.AddColorStop(0, color.RGBA{0x66, 0x7e, 0xea, 0xff})
+ grad.AddColorStop(1, color.RGBA{0x76, 0x4b, 0xa2, 0xff})
+
+ dc.SetFillStyle(grad)
+ dc.DrawRectangle(0, 0, imageWidth, imageHeight)
+ dc.Fill()
+
+ marginX := 80.0
+ marginY := 80.0
+
+ // App icon placeholder
+ dc.SetRGBA(1, 1, 1, 0.2)
+ dc.DrawRoundedRectangle(marginX, marginY, 96, 96, 22)
+ dc.Fill()
+
+ iconFace, err := loadFontFace(38, true)
+ if err != nil {
+ return nil, err
+ }
+ dc.SetFontFace(iconFace)
+ dc.SetRGBA(1, 1, 1, 0.9)
+ dc.DrawStringAnchored("PJ", marginX+48, marginY+52, 0.5, 0.5)
+
+ labelFace, err := loadFontFace(20, false)
+ if err != nil {
+ return nil, err
+ }
+ dc.SetFontFace(labelFace)
+ dc.SetRGBA(1, 1, 1, 0.8)
+ dc.DrawString("Portfolio Journal", marginX+120, marginY+34)
+
+ titleFace, err := loadFontFace(54, true)
+ if err != nil {
+ return nil, err
+ }
+ dc.SetFontFace(titleFace)
+ dc.SetRGBA(1, 1, 1, 1)
+ goalName = fitText(dc, goalName, imageWidth-160)
+ dc.DrawStringWrapped(goalName, marginX, marginY+130, 0, 0, imageWidth-160, 1.3, gg.AlignLeft)
+
+ valueFace, err := loadFontFace(36, true)
+ if err != nil {
+ return nil, err
+ }
+ labelSmallFace, err := loadFontFace(18, false)
+ if err != nil {
+ return nil, err
+ }
+
+ dc.SetFontFace(labelSmallFace)
+ dc.SetRGBA(1, 1, 1, 0.75)
+ dc.DrawString("Current", marginX, marginY+260)
+ dc.DrawString("Goal", marginX+420, marginY+260)
+
+ dc.SetFontFace(valueFace)
+ dc.SetRGBA(1, 1, 1, 1)
+ dc.DrawString(formattedReal, marginX, marginY+305)
+ dc.DrawString(formattedGoal, marginX+420, marginY+305)
+
+ percentText := "Progress unavailable"
+ if goalAmount > 0 {
+ percentText = fmt.Sprintf("%.0f%% complete", percent)
+ }
+ if data.PrivateMode {
+ percentText = strings.ReplaceAll(percentText, "complete", "of goal")
+ }
+
+ dc.SetFontFace(labelSmallFace)
+ dc.SetRGBA(1, 1, 1, 0.85)
+ dc.DrawString(percentText, marginX, marginY+360)
+
+ barWidth := 720.0
+ barHeight := 18.0
+ barX := marginX
+ barY := marginY + 380
+ progress := math.Max(0, math.Min(100, percent)) / 100
+
+ dc.SetRGBA(1, 1, 1, 0.25)
+ dc.DrawRoundedRectangle(barX, barY, barWidth, barHeight, 9)
+ dc.Fill()
+
+ dc.SetRGBA(1, 1, 1, 0.9)
+ dc.DrawRoundedRectangle(barX, barY, barWidth*progress, barHeight, 9)
+ dc.Fill()
+
+ meta := joinLabels(formatDateLabel("Target", data.TargetDate), formatDateLabel("Est. completion", data.EstimateCompletion))
+ if meta != "" {
+ dc.SetFontFace(labelSmallFace)
+ dc.SetRGBA(1, 1, 1, 0.7)
+ dc.DrawString(meta, marginX, marginY+430)
+ }
+
+ ctaFace, err := loadFontFace(20, true)
+ if err != nil {
+ return nil, err
+ }
+ dc.SetFontFace(ctaFace)
+ dc.SetRGBA(1, 1, 1, 0.9)
+ dc.DrawString("Available on the App Store", marginX, imageHeight-70)
+
+ buf := bytes.NewBuffer(nil)
+ if err := dc.EncodePNG(buf); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func numberToFloat(n json.Number) (float64, bool) {
+ if n.String() == "" {
+ return 0, false
+ }
+ value, err := n.Float64()
+ if err != nil {
+ return 0, false
+ }
+ return value, true
+}
+
+func formatCurrency(amount float64, code string, tag language.Tag) string {
+ unit, err := currency.ParseISO(code)
+ if err != nil {
+ unit = currency.USD
+ }
+ printer := message.NewPrinter(tag)
+ return printer.Sprintf("%v", currency.Amount(amount, unit))
+}
+
+func localeForCurrency(code string) language.Tag {
+ switch code {
+ case "EUR":
+ return language.MustParse("es-ES")
+ case "GBP":
+ return language.MustParse("en-GB")
+ case "JPY":
+ return language.MustParse("ja-JP")
+ case "CNY":
+ return language.MustParse("zh-CN")
+ case "HKD":
+ return language.MustParse("zh-HK")
+ case "KRW":
+ return language.MustParse("ko-KR")
+ case "INR":
+ return language.MustParse("en-IN")
+ case "BRL":
+ return language.MustParse("pt-BR")
+ case "MXN":
+ return language.MustParse("es-MX")
+ case "ARS":
+ return language.MustParse("es-AR")
+ case "CLP":
+ return language.MustParse("es-CL")
+ case "COP":
+ return language.MustParse("es-CO")
+ case "CAD":
+ return language.MustParse("en-CA")
+ case "AUD":
+ return language.MustParse("en-AU")
+ case "NZD":
+ return language.MustParse("en-NZ")
+ case "CHF":
+ return language.MustParse("de-CH")
+ case "SEK":
+ return language.MustParse("sv-SE")
+ case "NOK":
+ return language.MustParse("nb-NO")
+ case "DKK":
+ return language.MustParse("da-DK")
+ case "PLN":
+ return language.MustParse("pl-PL")
+ case "CZK":
+ return language.MustParse("cs-CZ")
+ case "HUF":
+ return language.MustParse("hu-HU")
+ case "TRY":
+ return language.MustParse("tr-TR")
+ case "ZAR":
+ return language.MustParse("en-ZA")
+ case "AED":
+ return language.MustParse("ar-AE")
+ default:
+ return language.MustParse("en-US")
+ }
+}
+
+func formatDateLabel(prefix, dateString string) string {
+ if dateString == "" {
+ return ""
+ }
+ parsed, err := time.Parse("2006-01-02", dateString)
+ if err != nil {
+ return ""
+ }
+ return fmt.Sprintf("%s: %s", prefix, parsed.Format("Jan 2, 2006"))
+}
+
+func joinLabels(primary, secondary string) string {
+ if primary == "" && secondary == "" {
+ return ""
+ }
+ if primary == "" {
+ return secondary
+ }
+ if secondary == "" {
+ return primary
+ }
+ return primary + " • " + secondary
+}
+
+func buildBaseURL(r *http.Request) string {
+ proto := r.Header.Get("X-Forwarded-Proto")
+ if proto == "" {
+ proto = "https"
+ }
+ host := r.Header.Get("X-Forwarded-Host")
+ if host == "" {
+ host = r.Host
+ }
+ return fmt.Sprintf("%s://%s", proto, host)
+}
+
+func escapeHTML(input string) string {
+ replacer := strings.NewReplacer(
+ "&", "&",
+ "<", "<",
+ ">", ">",
+ "\"", """,
+ "'", "'",
+ )
+ return replacer.Replace(input)
+}
+
+func loadFontFace(size float64, bold bool) (font.Face, error) {
+ var fontBytes []byte
+ if bold {
+ fontBytes = gobold.TTF
+ } else {
+ fontBytes = goregular.TTF
+ }
+ parsed, err := opentype.Parse(fontBytes)
+ if err != nil {
+ return nil, err
+ }
+ face, err := opentype.NewFace(parsed, &opentype.FaceOptions{
+ Size: size,
+ DPI: 72,
+ Hinting: font.HintingFull,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return face, nil
+}
+
+func fitText(dc *gg.Context, text string, maxWidth float64) string {
+ trimmed := strings.TrimSpace(text)
+ if trimmed == "" {
+ return ""
+ }
+ if width, _ := dc.MeasureString(trimmed); width <= maxWidth {
+ return trimmed
+ }
+
+ runes := []rune(trimmed)
+ for len(runes) > 0 {
+ candidate := string(runes) + "…"
+ if width, _ := dc.MeasureString(candidate); width <= maxWidth {
+ return candidate
+ }
+ runes = runes[:len(runes)-1]
+ }
+ return trimmed
+}
diff --git a/og-service/portfoliojournal-og.openrc b/og-service/portfoliojournal-og.openrc
new file mode 100644
index 0000000..1f65d3f
--- /dev/null
+++ b/og-service/portfoliojournal-og.openrc
@@ -0,0 +1,21 @@
+#!/sbin/openrc-run
+
+name="portfoliojournal-og"
+description="Portfolio Journal Open Graph share service"
+
+command="/usr/local/bin/portfoliojournal-og"
+command_args=""
+command_background="yes"
+output_log="/var/log/portfoliojournal-og.log"
+error_log="/var/log/portfoliojournal-og.err"
+pidfile="/run/portfoliojournal-og.pid"
+
+start_pre() {
+ checkpath --file --mode 0644 --owner root:root "$output_log"
+ checkpath --file --mode 0644 --owner root:root "$error_log"
+}
+
+depend() {
+ need net
+ after firewall
+}
diff --git a/website-og-image-spec.md b/website-og-image-spec.md
new file mode 100644
index 0000000..abd73f3
--- /dev/null
+++ b/website-og-image-spec.md
@@ -0,0 +1,74 @@
+# Open Graph Image Specification
+
+## Required Image: `og-share-card.png`
+
+Create this image and place it at: `https://portfoliojournal.app/images/og-share-card.png`
+
+### Dimensions
+- **Size:** 1200 x 630 pixels (Facebook/LinkedIn optimal)
+- **Format:** PNG or JPG
+- **File size:** Under 1MB recommended
+
+### Design Recommendations
+
+```
++----------------------------------------------------------+
+| |
+| [App Icon] |
+| |
+| Portfolio Journal |
+| ───────────────── |
+| |
+| Track your investment goals |
+| and celebrate your wins. |
+| |
+| [Progress bar illustration] |
+| |
+| ★ Goal Tracking ★ Smart Predictions |
+| ★ Portfolio Analytics ★ Privacy Focused |
+| |
+| [App Store Badge] |
+| |
++----------------------------------------------------------+
+```
+
+### Color Scheme
+- **Background:** Gradient from #667eea to #764ba2 (match your app branding)
+- **Text:** White
+- **Accent:** Your app's accent color
+
+### Elements to Include
+1. App icon (prominent, top-left or center)
+2. App name "Portfolio Journal"
+3. Tagline or value proposition
+4. Visual element (progress bar, chart icon, or screenshot)
+5. Feature highlights
+6. App Store badge (optional)
+
+### Tools to Create
+- Figma (free template available)
+- Canva
+- Adobe Illustrator/Photoshop
+
+## Testing Your OG Tags
+
+After deploying, test with these tools:
+- **Facebook:** https://developers.facebook.com/tools/debug/
+- **Twitter:** https://cards-dev.twitter.com/validator
+- **LinkedIn:** https://www.linkedin.com/post-inspector/
+- **General:** https://www.opengraph.xyz/
+
+## Files to Deploy
+
+1. `/share/index.html` (or configure your server to serve `share.html` at `/share`)
+2. `/images/og-share-card.png`
+3. `/images/app-icon.png` (your app icon, 120x120 or larger)
+
+## Apple Smart App Banner
+
+The HTML includes this meta tag:
+```html
+
+```
+
+This shows a smart banner on iOS Safari prompting users to download your app.
diff --git a/website-share-page.html b/website-share-page.html
new file mode 100644
index 0000000..989b77b
--- /dev/null
+++ b/website-share-page.html
@@ -0,0 +1,203 @@
+
+
+
+
+
+ Portfolio Journal - Track Your Investment Goals
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
Portfolio Journal
+
Track your investment goals and celebrate your financial milestones.
+
+
+ Goal Tracking
+ Portfolio Analytics
+ Smart Predictions
+ Privacy Focused
+
+
+
+
+ Download on App Store
+
+
+
+
+
+
+
+
+