Skip to main content

Overview

SpendWisely George integrates with Fold Money to automatically sync bank transactions, eliminating manual entry for bank-linked expenses. The integration uses the open-source Unfold CLI to fetch transactions from Fold’s Account Aggregator API.
Important: Fold Money is currently invite-only. You must have a Fold account and connect your banks through the Fold mobile app before using this feature.

Architecture

1

Fold Mobile App

User connects banks via Account Aggregator (regulated by RBI)
2

Unfold CLI

Backend binary fetches transactions and stores in SQLite
3

FastAPI Server

Python server exposes REST API for frontend
4

Frontend

Displays bank transactions and balance in real-time

Authentication Flow

1. Login with OTP

Users authenticate using their Fold phone number:
server.py
class LoginRequest(BaseModel):
    phone: str

@app.post("/api/fold/login")
def fold_login(req: LoginRequest):
    url = "https://api.fold.money/v1/auth/otp"
    
    # Format phone to +91... if not present
    phone = req.phone
    if not phone.startswith("+"):
        phone = "+91" + phone
        
    payload = {"phone": phone, "channel": "sms"}
    
    try:
        res = requests.post(url, json=payload)
        res.raise_for_status()
        return {"status": "otp_sent"}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

2. Verify OTP

After receiving OTP, verify and store credentials:
server.py
class VerifyRequest(BaseModel):
    phone: str
    otp: str

@app.post("/api/fold/verify")
def fold_verify(req: VerifyRequest):
    url = "https://api.fold.money/v1/auth/otp/verify"
    phone = req.phone
    if not phone.startswith("+"):
        phone = "+91" + phone

    payload = {"phone": phone, "otp": req.otp}
    
    try:
        res = requests.post(url, json=payload)
        res.raise_for_status()
        data = res.json().get("data", {})
        
        access = data.get("access_token")
        refresh = data.get("refresh_token")
        uuid = data.get("user_meta", {}).get("uuid")
        
        if access and refresh:
            save_unfold_config(access, refresh, uuid)
            return {"status": "success"}
        else:
            raise HTTPException(status_code=400, detail="Invalid response from Fold")
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Verification failed: {str(e)}")

3. Store Credentials

Tokens are saved to a YAML config file for Unfold CLI:
server.py
import yaml
import os

UNFOLD_CONFIG = "./unfold_config.yaml"

def save_unfold_config(access_token, refresh_token, uuid):
    config = {
        "token": {
            "access": access_token,
            "refresh": refresh_token
        },
        "fold_user": {
            "uuid": uuid
        },
        "device_hash": "python-client-" + os.urandom(4).hex()
    }
    with open(UNFOLD_CONFIG, "w") as f:
        yaml.dump(config, f)

Transaction Syncing

Sync Endpoint

Manual sync triggered by user clicking “SYNC BANK” button:
server.py
UNFOLD_BINARY = "./unfold/unfold"
UNFOLD_CONFIG = "./unfold_config.yaml"
DB_PATH = "./unfold/db.sqlite"

@app.post("/api/sync")
def sync_transactions():
    try:
        subprocess.run([
            UNFOLD_BINARY, 
            "transactions", 
            "--db", 
            "--config", 
            UNFOLD_CONFIG
        ], check=True)
        return {"status": "success"}
    except subprocess.CalledProcessError as e:
        raise HTTPException(
            status_code=500, 
            detail="Failed to sync. Ensure you are logged in."
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

Frontend Sync Flow

index.html
async function syncBank() {
    setStatus("SYNCING BANK...", "text-purple-500", "bg-purple-500");
    
    try {
        const res = await fetch('/api/sync', { method: 'POST' });
        if (res.ok) {
            setTimeout(() => {
                fetchBankTransactions();
                setStatus("ONLINE", "text-green-500", "bg-green-500");
            }, 5000); // Wait a bit for unfold to finish
        }
    } catch (e) {
        setStatus("SYNC FAILED", "text-red-500", "bg-red-500");
    }
}

Fetching Transactions

Backend API

Reads from SQLite database populated by Unfold CLI:
server.py
import sqlite3

@app.get("/api/transactions")
def get_transactions():
    """Fetches transactions from the Unfold SQLite DB."""
    if not os.path.exists(DB_PATH) or get_unfold_token():
        if not os.path.exists(DB_PATH):
            try:
                subprocess.run([
                    UNFOLD_BINARY, 
                    "transactions", 
                    "--db", 
                    "--config", 
                    UNFOLD_CONFIG
                ], check=True)
            except:
                pass  # Might fail if not logged in

    if not os.path.exists(DB_PATH):
        return []

    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    
    try:
        cursor.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='transactions'"
        )
        if not cursor.fetchone():
            return []

        cursor.execute("""
            SELECT uuid, amount, current_balance, timestamp, type, account, merchant 
            FROM transactions 
            ORDER BY timestamp DESC 
            LIMIT 50
        """)
        rows = cursor.fetchall()
        
        results = []
        for row in rows:
            results.append({
                "uuid": row[0],
                "amount": row[1],
                "current_balance": row[2],
                "timestamp": row[3],
                "type": row[4],
                "account": row[5],
                "merchant": row[6]
            })
        return results
    except Exception as e:
        print(f"DB Error: {e}")
        return []
    finally:
        conn.close()

Frontend Display

index.html
async function fetchBankTransactions() {
    try {
        const res = await fetch('/api/transactions');
        const data = await res.json();
        const list = document.getElementById('bankTxList');
        document.getElementById('bankLoader').classList.add('hidden');

        if (Array.isArray(data) && data.length > 0) {
            let bankBalance = data[0].current_balance || 0;
            appData.bankBalance = bankBalance;

            list.innerHTML = '';
            data.slice(0, 10).forEach(tx => {
                const date = new Date(tx.timestamp).toLocaleDateString();
                const isCredit = tx.amount > 0;
                const color = isCredit ? 'text-green-600' : 'text-slate-900';
                
                list.innerHTML += `
                    <div class="card p-3 bg-white flex justify-between items-center">
                        <div class="flex items-center gap-3">
                            <div class="w-8 h-8 bg-purple-50 rounded-lg flex items-center justify-center text-sm border border-purple-100">
                                🏦
                            </div>
                            <div>
                                <p class="font-bold text-xs text-slate-800">
                                    ${tx.merchant || tx.description}
                                </p>
                                <p class="text-[9px] font-bold text-slate-400 uppercase">
                                    ${date}
                                </p>
                            </div>
                        </div>
                        <p class="font-black ${color} text-sm">
                            ${isCredit ? '+' : ''}${Math.abs(tx.amount)}
                        </p>
                    </div>
                `;
            });
        } else {
            list.innerHTML = '<p class="text-xs text-slate-400 text-center py-4">No bank data found. Try syncing.</p>';
        }
        renderUI();
    } catch (e) {
        console.error("Bank Fetch Error", e);
        document.getElementById('bankLoader').innerHTML = 
            '<span class="text-red-500 text-xs">Error</span>';
    }
}

Balance Tracking

The most recent transaction’s current_balance field is used:
index.html
if (Array.isArray(data) && data.length > 0) {
    let bankBalance = data[0].current_balance || 0;
    appData.bankBalance = bankBalance;
    
    // Update UI card
    updateDisplay('uiBankBal', appData.bankBalance || 0);
}

Unfold CLI Details

From the Unfold README:
# Login to Fold account
$ unfold login

# Fetch transactions
$ unfold transactions

# Fetch and save to SQLite
$ unfold transactions --db

# Fetch with date range
$ unfold transactions -s 2023-09-20 --db

# Run as daemon (fetch every 20 seconds)
$ unfold transactions --db -w '@every 20s'
Key Points:
  • Uses Fold Money’s unofficial API (MITM’d from mobile app)
  • Stores transactions in SQLite with schema: uuid, amount, current_balance, timestamp, type, account, merchant
  • Supports cron-like scheduling for auto-sync
  • Config stored in ~/.config/unfold/config.yaml by default

Database Schema

Unfold creates a SQLite table with this structure:
ColumnTypeDescription
uuidTEXTUnique transaction ID
amountREALTransaction amount (positive = credit)
current_balanceREALBalance after transaction
timestampTEXTISO 8601 timestamp
typeTEXTTransaction type
accountTEXTBank account identifier
merchantTEXTMerchant/payee name

UI Components

Bank Balance Card

index.html
<div class="card p-4 bg-white">
    <p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">
        Bank Balance
    </p>
    <h3 id="uiBankBal" class="text-xl font-black text-slate-800">₹0</h3>
    <p class="text-[10px] text-purple-500 font-bold mt-1">Fold Connected</p>
</div>

Transaction List

index.html
<div class="px-5 mt-8">
    <h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4 ml-1">
        Recent Bank Data (Fold)
    </h3>
    <div id="bankTxList" class="space-y-3 pb-6 relative">
        <div class="absolute inset-0 bg-white/50 backdrop-blur-[1px] z-10 flex items-center justify-center"
            id="bankLoader">
            <span class="text-[10px] font-bold text-slate-400">Loading...</span>
        </div>
    </div>
</div>

Sync Button

index.html
<button onclick="syncBank()" id="syncBtn"
    class="text-xs font-bold text-purple-600 bg-purple-50 px-4 py-2 rounded-full hover:bg-purple-100 transition-colors">
    SYNC BANK
</button>

Settings Panel Integration

Fold Login UI

index.html
<div class="p-4 bg-purple-50 rounded-2xl border border-purple-100">
    <h3 class="font-bold text-purple-900 mb-2">Connect Fold</h3>
    
    <div id="foldLoginInputs">
        <input type="tel" id="foldPhone" placeholder="Phone (99...)"
            class="w-full p-3 bg-white rounded-xl mb-2 text-sm">
        <button onclick="sendFoldOtp()"
            class="w-full bg-purple-600 text-white p-3 rounded-xl font-bold text-sm">
            Send OTP
        </button>
    </div>
    
    <div id="foldOtpInputs" class="hidden">
        <input type="text" id="foldOtp" placeholder="Enter OTP"
            class="w-full p-3 bg-white rounded-xl mb-2 text-sm text-center tracking-widest">
        <button onclick="verifyFoldOtp()"
            class="w-full bg-green-600 text-white p-3 rounded-xl font-bold text-sm">
            Verify & Login
        </button>
    </div>
    
    <p id="foldStatusMsg" class="text-[10px] text-center mt-2 font-bold text-purple-400"></p>
</div>

JavaScript Handlers

index.html
async function checkFoldStatus() {
    try {
        const res = await fetch('/api/fold/status');
        const data = await res.json();
        if (data.logged_in) {
            document.getElementById('foldLoginInputs').classList.add('hidden');
            document.getElementById('foldStatusMsg').innerText = "✅ Logged in to Fold";
        }
    } catch (e) { }
}

async function sendFoldOtp() {
    const phone = document.getElementById('foldPhone').value;
    try {
        const res = await fetch('/api/fold/login', {
            method: 'POST', 
            body: JSON.stringify({ phone: phone }),
            headers: { 'Content-Type': 'application/json' }
        });
        if (res.ok) {
            document.getElementById('foldLoginInputs').classList.add('hidden');
            document.getElementById('foldOtpInputs').classList.remove('hidden');
            document.getElementById('foldStatusMsg').innerText = "OTP Sent!";
        }
    } catch (e) { 
        alert("Failed to send OTP"); 
    }
}

async function verifyFoldOtp() {
    const phone = document.getElementById('foldPhone').value;
    const otp = document.getElementById('foldOtp').value;
    try {
        const res = await fetch('/api/fold/verify', {
            method: 'POST', 
            body: JSON.stringify({ phone: phone, otp: otp }),
            headers: { 'Content-Type': 'application/json' }
        });
        if (res.ok) {
            document.getElementById('foldOtpInputs').classList.add('hidden');
            document.getElementById('foldStatusMsg').innerText = "✅ Successfully Logged In!";
            syncBank();
        } else { 
            alert("Verification Failed"); 
        }
    } catch (e) { 
        alert("Error verifying OTP"); 
    }
}

Security Considerations

Session Management: Unfold uses the same session as the Fold mobile app. Logging in via Unfold will log you out of the mobile app.
Token Storage: Access and refresh tokens are stored in plaintext YAML. In production, use encrypted storage.
API Stability: Fold’s API is not public. Future changes to Fold’s backend may break the integration.

Troubleshooting

Check:
  • Unfold binary is in ./unfold/ directory
  • You’ve logged in via Settings panel
  • Config file exists at ./unfold_config.yaml
Debug:
./unfold/unfold transactions --config ./unfold_config.yaml --db
Verify:
  • SQLite database exists: ./unfold/db.sqlite
  • Banks are connected in Fold mobile app
  • Date range includes recent transactions
Check database:
sqlite3 ./unfold/db.sqlite "SELECT COUNT(*) FROM transactions;"
The balance is taken from the current_balance field of the most recent transaction. If transactions are out of order, this may be inaccurate.
  • Verify phone number format (should be +91…)
  • Check Fold service status
  • Ensure you have an active Fold account

Best Practices

Sync regularly

Click SYNC BANK daily to keep transactions up to date

Cross-reference

Compare synced transactions with manual entries to avoid duplicates

Monitor balance

Total balance = Sheet balance + Bank balance + Portfolio value

Backup data

Export CSV regularly as Unfold only stores last 50 transactions

Build docs developers (and LLMs) love