An AI-powered operations layer on top of Wallet-Automation-Engine.
Users interact with it using natural language:
- "What's my balance?"
- "Show me last week's transactions"
- "Transfer $100 to wallet 42"
The agent plans and executes the required operations through the Wallet API.
It never accesses SQL Server directly and never bypasses the wallet's own service layer.
A money-moving AI agent must handle the same concerns as a production backend system:
- Authentication and authorization
- Audit trails
- Confirmation before irreversible operations
- Preventing the model from controlling sensitive identifiers
- Never letting a network hiccup turn into a duplicate transfer
- A small, unambiguous set of tools instead of one per endpoint
A key design decision:
No tool accepts a walletId or userId parameter from the planner.
The agent never trusts identifiers generated by the LLM.
The user's wallet identity is always resolved from the authenticated JWT.
The only tool that receives an identifier is TransferMoney, where the identifier represents the recipient wallet.
The source wallet is always derived from the caller's identity.
Reads and writes don't share a retry policy. A timeout means the outcome
is unknown, not that the operation failed. Read-only tools retry freely on
failure. State-changing tools (DepositMoney, WithdrawMoney,
TransferMoney) get exactly one attempt per call and are protected instead by
an idempotency key: ExecutionEngine computes a deterministic key (user +
tool + parameters) when a confirmation is requested, ToolExecutor checks
Redis for an already-recorded result before executing, and records the result
immediately on success. A genuine retry - the user re-approving, the agent
re-planning - reuses the same key and gets the original result back instead of
moving money twice.
Tools are consolidated by responsibility, not by endpoint. Reads that only differ by which filters are set are one tool with an optional payload, not several tools the model has to choose between:
QueryTransactions- passtransactionIdfor a single lookup, any oftype/minAmount/maxAmountto filter, or nothing for full history.AnalyzeFinances- passperiod: "month"orperiod: "window"(withdays), or nothing for all-time statistics.
The routing between those cases is a deterministic switch in C#, not a
decision the LLM has to make.
The agent has no registration endpoint and no user store of its own.
POST /api/auth/login forwards credentials to the Wallet API's own login endpoint and returns the same JWT.
Example:
POST /api/auth/login{
"email": "user@example.com",
"password": "password"
}Response:
{
"token": "<jwt-token>"
}Anyone who can authenticate with the wallet system can use the agent with that token.
Use it as:
Authorization: Bearer <token>on every /api/agent/* request.
Run Wallet-Automation-Engine and note its base URL.
docker compose up -dThis starts:
- Redis
- Ollama
Update:
{
"WalletApi": {
"BaseUrl": "<wallet-api-url>"
}
}The Agent doesn't issue its own tokens - it validates the JWT that the Wallet API
issued at login, using the same Jwt:Key / Issuer / Audience. That key
must never live in appsettings.json (it would end up committed to git), so
appsettings.json only has empty placeholders and the app will refuse to start
without a real key configured via user secrets.
Generate one key and set it, identically, in both projects:
# From Wallet-Operations-Agent/Wallet.Agent.Api
dotnet user-secrets set "Jwt:Key" "<your-generated-key>"
# From Wallet-Automation-Engine's API project
dotnet user-secrets init # only if it doesn't already have a UserSecretsId
dotnet user-secrets set "Jwt:Key" "<the-same-generated-key>"If Issuer or Audience ever differ between the two projects, add those the
same way (dotnet user-secrets set "Jwt:Issuer" "...") - but by default both
projects already agree on WalletApi / WalletApiUsers in appsettings.json,
so usually only Jwt:Key needs to be set.
On Windows, user secrets are stored at:
%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json
The browser-based chat client (see below) calls the API from a different
origin, so it needs to be allow-listed. In appsettings.Development.json:
{
"Cors": {
"AllowedOrigins": [ "http://localhost:5500" ]
}
}Adjust the port to whatever you actually serve the chat UI on.
dotnet run --project Wallet.Agent.ApiBy default this listens on http://localhost:5274 (see launchSettings.json).
wallet-agent-chat.html is a single, dependency-free file - login screen,
chat, confirmation approve/decline, and a sidebar of past conversations
pulled from Redis. It needs to be served over HTTP, not opened directly as a
file:// URL (the browser blocks that under CORS).
# from the folder containing wallet-agent-chat.html
dotnet tool install --global dotnet-serve # first time only
dotnet-serve -p 5500Then open http://localhost:5500/wallet-agent-chat.html. Make sure the port
here matches what you put in Cors:AllowedOrigins above.
POST /api/auth/login{
"email": "user@example.com",
"password": "password"
}POST /api/agent/chat{
"conversationId": "abc",
"question": "What's my balance?"
}For operations that require approval:
POST /api/agent/confirm{
"conversationId": "abc",
"approved": true
}Returns the caller's own conversations, most recently active first, with a preview of the last message. Backed by a Redis index kept alongside the existing per-conversation message lists.
GET /api/agent/conversationsGET /api/agent/conversations/{conversationId}/history{conversationId} is the same short id the client generates per session (not
prefixed with the user id - that scoping happens server-side from the JWT).