In a developer-first smart environment, we bridge physical sensors, AC systems, and mesh radios. But we can go further: connecting financial APIs directly to our terminal workspace. By integrating the Wise (formerly TransferWise) API with Antigravity (agy) and Home Assistant, we can check multi-currency account balances and execute secure payments directly from our terminal or off-grid radio mesh.
This guide details how this integration works under the hood, how the Python CLI script handles Wise’s Strong Customer Authentication (SCA), and how Home Assistant and agy are configured to interact with it.
1. The Architecture: Local Terminal to Financial Gateway
Connecting financial accounts to an AI agent requires extreme security. We utilize a Personal Access Token with restricted scopes generated in the Wise developer dashboard, combined with RSA private key signing for 2-Factor Authentication (2FA).

- Direct CLI Interface: The developer or the AI assistant runs
/home/gvoina/scripts/wise_wallet.pydirectly from the shell. - Home Assistant Dashboard: Home Assistant runs the same script mounted inside its container at
/config/wise_wallet.pyto poll balances every hour. - Cryptographic 2FA Signing: When a payment is initiated, Wise’s gateway challenges the request. The script intercepts this, signs it using a local private key (
wise_private_key.pem), and completes the transfer automatically.
2. The CLI Bridge: wise_wallet.py
This script, located at wise_wallet.py, handles all requests. Crucially, it manages the SCA (Strong Customer Authentication) signing loop:
- The script makes a
POSTrequest to fund a transfer. - The Wise API returns an
HTTP 403 Forbiddenwith a header namedx-2fa-approvalcontaining a one-time challenge UUID. - The script reads a local private RSA key (
/home/gvoina/scripts/wise_private_key.pem), signs the challenge UUID usingSHA-256andPKCS1v15padding, and encodes it in Base64. - The script re-submits the request with the headers
x-2fa-approvalandx-2fa-signatureto authorize and execute the payment.
Here is the complete code of the CLI bridge:
#!/usr/bin/env python3
# =============================================================================
# Wise (formerly TransferWise) API CLI Wallet Bridge
# Manages multi-currency balance queries and initiates transfers.
# Support for 2FA signature signing via a local private key.
# =============================================================================
import os
import sys
import json
import uuid
import requests
import base64
script_dir = os.path.dirname(os.path.realpath(__file__))
def get_path(default_path, filename):
if os.path.exists(default_path):
return default_path
return os.path.join(script_dir, filename)
TOKEN_PATH = get_path("/home/gvoina/scripts/wise_token.txt", "wise_token.txt")
PROFILE_PATH = get_path("/home/gvoina/scripts/wise_profile.txt", "wise_profile.txt")
KEY_PATH = get_path("/home/gvoina/scripts/wise_private_key.pem", "wise_private_key.pem")
ENV_PATH = get_path("/home/gvoina/scripts/wise_env.txt", "wise_env.txt")
def get_env():
if os.path.exists(ENV_PATH):
try:
with open(ENV_PATH, "r", encoding="utf-8") as f:
return f.read().strip().lower()
except Exception:
pass
return os.getenv("WISE_ENV", "production").lower()
def get_base_url():
env = get_env()
if env == "sandbox":
return "https://api.sandbox.transferwise.tech"
return "https://api.transferwise.com"
def get_token():
token = os.getenv("WISE_TOKEN")
if not token and os.path.exists(TOKEN_PATH):
try:
with open(TOKEN_PATH, "r", encoding="utf-8") as f:
token = f.read().strip()
except Exception as e:
print(f"Error reading token file: {e}", file=sys.stderr)
return token
def get_profile_id(headers, base_url):
if os.path.exists(PROFILE_PATH):
try:
with open(PROFILE_PATH, "r", encoding="utf-8") as f:
return int(f.read().strip())
except Exception:
pass
try:
url = f"{base_url}/v1/profiles"
res = make_request("GET", url, headers)
for profile in res:
if profile.get("type") == "personal":
profile_id = profile.get("id")
with open(PROFILE_PATH, "w", encoding="utf-8") as f:
f.write(str(profile_id))
return profile_id
if res:
profile_id = res[0].get("id")
with open(PROFILE_PATH, "w", encoding="utf-8") as f:
f.write(str(profile_id))
return profile_id
except Exception as e:
print(f"Error fetching profiles: {e}", file=sys.stderr)
return None
def sign_2fa_token(token):
try:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
except ImportError:
print("Error: cryptography library is required for 2FA signing.", file=sys.stderr)
sys.exit(1)
if not os.path.exists(KEY_PATH):
print(f"Error: 2FA signing key not found at {KEY_PATH}.", file=sys.stderr)
sys.exit(1)
try:
with open(KEY_PATH, "rb") as f:
private_key = load_pem_private_key(f.read(), password=None)
signature = private_key.sign(
token.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256()
)
return base64.b64encode(signature).decode("utf-8")
except Exception as e:
print(f"Failed to sign 2FA token: {e}", file=sys.stderr)
sys.exit(1)
def make_request(method, url, headers, json_data=None):
res = requests.request(method, url, headers=headers, json=json_data, timeout=15)
if res.status_code == 403 and "x-2fa-approval" in res.headers:
approval_token = res.headers["x-2fa-approval"]
signature = sign_2fa_token(approval_token)
new_headers = headers.copy()
new_headers["x-2fa-approval"] = approval_token
new_headers["x-2fa-signature"] = signature
res = requests.request(method, url, headers=new_headers, json=json_data, timeout=15)
res.raise_for_status()
return res.json() if res.content else {}
def cmd_balances(headers, base_url):
profile_id = get_profile_id(headers, base_url)
if not profile_id:
print("Error: Could not retrieve Wise profile ID.", file=sys.stderr)
sys.exit(1)
url = f"{base_url}/v4/profiles/{profile_id}/balances?types=STANDARD"
try:
balances = make_request("GET", url, headers)
print(json.dumps(balances, indent=2))
except Exception as e:
print(f"Error querying balances: {e}", file=sys.stderr)
sys.exit(1)
def cmd_make_payment(headers, base_url, recipient_name, recipient_iban, amount, currency, reference):
profile_id = get_profile_id(headers, base_url)
if not profile_id:
print("Error: Could not retrieve Wise profile ID.", file=sys.stderr)
sys.exit(1)
# 1. Create a quote
print("Step 1: Creating transfer quote...")
quote_url = f"{base_url}/v3/profiles/{profile_id}/quotes"
quote_data = {
"sourceCurrency": currency,
"targetCurrency": currency,
"sourceAmount": float(amount),
"targetAmount": None,
"payOut": "BALANCE"
}
try:
quote = make_request("POST", quote_url, headers, quote_data)
quote_id = quote.get("id")
print(f" Quote created: UUID {quote_id}")
except Exception as e:
print(f"Failed to create quote: {e}", file=sys.stderr)
sys.exit(1)
# 2. Create recipient
print("Step 2: Locating or creating recipient...")
recipient_url = f"{base_url}/v1/recipients"
recipient_data = {
"profile": profile_id,
"type": "iban",
"currency": currency,
"accountHolderName": recipient_name,
"details": {
"legalType": "PRIVATE",
"iban": recipient_iban
}
}
try:
recipient = make_request("POST", recipient_url, headers, recipient_data)
recipient_id = recipient.get("id")
print(f" Recipient created: ID {recipient_id}")
except Exception as e:
print(f"Failed to create recipient: {e}", file=sys.stderr)
sys.exit(1)
# 3. Create transfer
print("Step 3: Creating transfer transaction...")
transfer_url = f"{base_url}/v1/transfers"
transaction_id = str(uuid.uuid4())
transfer_data = {
"targetAccount": recipient_id,
"quoteUuid": quote_id,
"customerTransactionId": transaction_id,
"details": {
"reference": reference
}
}
try:
transfer = make_request("POST", transfer_url, headers, transfer_data)
transfer_id = transfer.get("id")
print(f" Transfer created: ID {transfer_id}")
except Exception as e:
print(f"Failed to create transfer: {e}", file=sys.stderr)
sys.exit(1)
# 4. Fund the transfer from standard balance
print("Step 4: Funding transfer from balance (SCA signature if required)...")
fund_url = f"{base_url}/v3/transfers/{transfer_id}/payments"
fund_data = {
"type": "BALANCE"
}
try:
payment = make_request("POST", fund_url, headers, fund_data)
print("✅ Transfer successfully funded and executed!")
print(json.dumps(payment, indent=2))
except Exception as e:
print(f"Failed to fund transfer: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
token = get_token()
if not token:
print("Error: Wise API personal token not configured.", file=sys.stderr)
print(f"Please save your token in {TOKEN_PATH} or export it as WISE_TOKEN.", file=sys.stderr)
sys.exit(1)
base_url = get_base_url()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
if len(sys.argv) < 2:
print("Usage:")
print(" Query balances: python3 wise_wallet.py balance")
print(" Make payment: python3 wise_wallet.py payment <recipient_name> <recipient_iban> <amount> [currency] [reference]")
sys.exit(1)
cmd = sys.argv[1].lower()
if cmd == "balance":
cmd_balances(headers, base_url)
elif cmd == "payment":
if len(sys.argv) < 5:
print("Usage: python3 wise_wallet.py payment <recipient_name> <recipient_iban> <amount> [currency] [reference]", file=sys.stderr)
sys.exit(1)
name = sys.argv[2]
iban = sys.argv[3]
amount = sys.argv[4]
currency = sys.argv[5] if len(sys.argv) > 5 else "EUR"
ref = sys.argv[6] if len(sys.argv) > 6 else "Sent via Antigravity"
cmd_make_payment(headers, base_url, name, iban, amount, currency, ref)
else:
print(f"Unknown command: {cmd}", file=sys.stderr)
sys.exit(1)
3. Home Assistant Configuration
To fetch balances automatically, we set up Command Line Sensors inside /home/gvoina/homeassistant/configuration.yaml.
Mount Path Considerations
Because Home Assistant runs in a Docker container, it cannot access the host directory /home/gvoina/scripts/ directly. We copy the script and its credential configuration files to the /home/gvoina/homeassistant/ directory. Since this folder is mounted inside the container as /config/, the files become available as /config/wise_wallet.py, /config/wise_token.txt, and /config/wise_profile.txt.
The script’s get_path function handles this transition seamlessly by falling back to its own directory when absolute host paths are missing.
configuration.yaml Snippet
command_line:
- sensor:
name: "Wise EUR Balance"
command: "python3 /config/wise_wallet.py balance 2>/dev/null"
value_template: >
{% set eur = value_json | selectattr('currency', 'eq', 'EUR') | list | first %}
{{ eur.amount.value if eur is defined and eur.amount is defined else 0 }}
unit_of_measurement: "EUR"
scan_interval: 3600
- sensor:
name: "Wise USD Balance"
command: "python3 /config/wise_wallet.py balance 2>/dev/null"
value_template: >
{% set usd = value_json | selectattr('currency', 'eq', 'USD') | list | first %}
{{ usd.amount.value if usd is defined and usd.amount is defined else 0 }}
unit_of_measurement: "USD"
scan_interval: 3600
- sensor:
name: "Wise RON Balance"
command: "python3 /config/wise_wallet.py balance 2>/dev/null"
value_template: >
{% set ron = value_json | selectattr('currency', 'eq', 'RON') | list | first %}
{{ ron.amount.value if ron is defined and ron.amount is defined else 0 }}
unit_of_measurement: "RON"
scan_interval: 3600
4. Workspace Customizations (AGENTS.md)
To teach agy how to use these commands, the following instructions are loaded into the agentic rulebook at .agents/AGENTS.md:
### Wise (TransferWise) Wallet Operations
You can query account balances and execute payments using:
* **Query balances**: `python3 /home/gvoina/scripts/wise_wallet.py balance`
* **Execute payment/transfer**: `python3 /home/gvoina/scripts/wise_wallet.py payment <recipient_name> <recipient_iban> <amount> [currency] [reference]`
* *Tip*: This supports automatic 2FA signing if the private key `wise_private_key.pem` is configured in `/home/gvoina/scripts/`.
### Operating Procedures:
* **Financial Transactions**: For any payment or transfer command, always ask the developer for confirmation and explain the transfer details before executing it.
5. Security & Human-in-the-Loop Verification
Financial integrations require strict safety boundaries:
- Interactive Review:
agywill never execute a payment without presenting the recipient name, IBAN, amount, and reference to the developer first, requesting explicit confirmation. - Local Key Storage: The private RSA key (
wise_private_key.pem) is kept strictly in the local workspace. No key material is transmitted over external channels.
6. Real-World Shell Session Example
Here is a mock shell output showing how the agent checks balances and handles a payment request:
Check Balances
Developer:
Check my wallet balances.agy:
I will query your Wise multi-currency account.
[agy runspython3 /home/gvoina/scripts/wise_wallet.py balance]agy:
You currently have **0.01 EUR**, **0.00 USD**, and **0.00 RON** in your accounts.
Execute Payment
Developer:
Pay 5 EUR to John Doe (DE12345678901234567890) for rent.agy:
⚠️ This is a financial transfer. Please review the details below:
- Recipient:
John Doe- IBAN:
DE12345678901234567890- Amount:
5.00 EUR- Reference:
rent
Do you approve executing this transfer?Developer:
Yes, execute.
[agy runspython3 /home/gvoina/scripts/wise_wallet.py payment "John Doe" "DE12345678901234567890" "5.00" "EUR" "rent"]
[The script intercepts Wise’s 2FA challenge, signs it using the private key, and submits the signature]agy:
✅ Transfer successfully executed! Reference ID: 2548912
7. Conclusion
Integrating the Wise API with Home Assistant and agy creates a secure financial dashboard directly in your workspace. By utilizing a local CLI bridge combined with RSA key-pair signatures, we enable balance checking and money transfers without sacrificing security, turning your coding terminal into a fully capable bank terminal.