A design tool used twice a month can look like wasted money.
Unless those two sessions are when you deliver work to a client.
That is the gap I wanted to explore with SubSense: subscription trackers can tell you what you pay, but a useful renewal decision also needs to remember why you kept paying.
I'm building SubSense around a simple idea:
Remember the reason behind a decision, then reconsider it when the facts change.
The project is being prepared for the Tatum × Walrus Total Recall hackathon, with Consumer Finance Apps as its intended track. This post covers the working local experience, the integration design, and the live checks still outstanding.
Start with clarity, not another setup screen
Actual application UI with synthetic demonstration records-not live billing accounts or Mainnet evidence.
The first useful screen is a dashboard, not a request for provider API keys.
After signed wallet login, users can add subscriptions, review recurring costs, set a monthly budget, and see what is renewing. Online memory and AI are optional.
Some details matter more than they initially seem:
- An annual plan is spread across 12 months when comparing commitments, but its upcoming bill shows the full charge.
- A trial includes its future price, so “free today” does not hide next month's commitment.
- A cheaper-plan or cancellation decision creates a follow-up. It does not pretend the provider has changed your account.
- Budget scenarios are previews until the user explicitly records a change.
SubSense does not connect to a bank, cancel subscriptions, or send payments. It helps users decide; it does not act as if the decision has already happened.
What “learning” means here
I wanted the core learning loop to be inspectable, rather than asking a model to invent a financial recommendation.
In SubSense, learning means structured decision feedback. It is not model training or fine-tuning.
Here is the example used in the browser review:
| Step | Information | Result |
|---|---|---|
| Before feedback | A $15/month design tool is rarely used and marked optional | Try a cheaper plan |
| User decision | Keep + Essential, with “Required for monthly client handoffs. Low usage does not mean low value.” | Save the decision and its reason |
| Next recommendation | The relevant saved decision is available | Keep, with the remembered context |
| New price | The same plan rises from $15 to $17-more than 10% | Request a fresh review |
The feedback does not silently rewrite the service's importance or billing status. It changes the recommendation, not the underlying facts.
Synthetic records in the real Renewals screen. The recommendation change was verified locally; this screenshot does not demonstrate remote recall.
The recommendation engine uses the latest applicable decision within a 180-day relevance window. It ignores unrelated and future-dated records, and a price increase above 10% can reopen the question. Services explicitly marked essential remain protected.
That last point is important: memory should inform a decision, not freeze it forever.
A small stack with clear boundaries
The application uses React, TypeScript, Vite, and Express, with one server process and one port.
React + TypeScript
├─ Wallet-scoped workspace → browser storage
├─ Provider vault → browser-side encryption
└─ Same-origin API requests
Express → verified wallet session
├─ Tatum → read-only Sui data
├─ Walrus Memory → explicit decision save / recall
└─ OpenRouter → optional, consented AI assistance
Subscription calculations and recommendations remain deterministic. USD amounts use integer cents; native-SUI amounts use integer MIST. A USD budget is not mixed with a SUI balance as though they were interchangeable.
The preview environment has no durable runtime database. Browser-local state makes the tracking experience work within that constraint, but it also creates an obligation: recovery must be explicit and understandable.
A wallet is identity-not an automatic vault unlock
Connecting a wallet is only the first step. The backend verifies a one-time personal-message signature before creating a session. Signing in costs no gas and does not authorize spending.
The workspace and provider credentials have different storage boundaries:
| Data | Where it lives | Important limitation |
|---|---|---|
| Subscriptions and local decisions | Wallet-scoped browser storage | Unencrypted; not synchronized across devices |
| Saved provider credentials | Encrypted browser vault | Requires the original wallet identity and separate password |
| Unlocked provider credentials | Backend's temporary in-memory session | Available to the app server while unlocked |
| Selected online decisions | Walrus Memory integration | Explicit upload; not a complete workspace backup |
The vault uses browser Web Crypto: PBKDF2-SHA-256 with 600,000 iterations, a fresh salt, and AES-256-GCM with a fresh IV. Authenticated additional data binds the ciphertext to the normalized wallet identity and Mainnet.
The password stays in the browser. Unlocking sends the decrypted provider keys to the app's backend; it is not a claim that the backend can never access them.
Signing out or locking removes backend credentials without deleting the saved workspace or ciphertext. Reconnecting the same wallet restores its local context, but the provider vault still needs its password.
Two exports are therefore necessary when moving devices:
- Workspace backup: subscriptions, decisions, and the original profile ID.
- Encrypted-vault backup: provider credentials, still protected by its password.
Wallet scoping is not a defence against someone or a malicious script with access to the browser. There is no vault-password recovery, and clearing storage without backups can lose local data.
Why the Walrus integration stores decisions
The useful memory is not just “this service costs $15.”
It is the combination of the service, price, decision, value assessment, timestamp, and reason. That gives future recommendations context they can actually use.
SubSense's Walrus Memory integration stores validated decision records in a profile-specific namespace:
subsense-<workspace-profile-id>
This means preserving the profile ID matters. Creating a new workspace is not the same as reopening the previous profile's memories.
The implemented upload path uses a profile-and-decision idempotency key. A simplified excerpt of the SDK call looks like this:
client.remember(JSON.stringify(decision), namespace, {
idempotencyKey: `${namespace}:${decision.id}`,
dedupe: true,
});
Uploads require confirmation. An accepted job is shown as pending, not as a successful Mainnet memory. The application records completed storage only when the job reports done and returns a blob ID for the matching namespace.
Recall is also explicit. Returned text is validated as decision data—not treated as instructions-and valid records merge by decision ID rather than duplicating history. Recall is bounded to 50 relevant results, so it is not an account-wide export or full recovery system.
There is also a privacy boundary worth stating plainly: the relayer processes decision plaintext before encrypted storage. “Encrypted storage” does not mean nobody involved in the upload can see the content. Private client details and secrets do not belong in public demonstration memories.
What Tatum contributes
Tatum's role is the read-only Sui data layer, separate from the subscription recommendation engine.
The Mainnet adapter is implemented for native-SUI balance reads and verification of existing transactions through Tatum's Sui GraphQL endpoint.
A transaction check is deliberately strict: the digest must match, execution must succeed, balance-change data must be complete, and the recipient's net increase must satisfy the requested amount. The check also validates the UTC payment date and an optional sender.
That is transfer evidence, not proof that a subscription vendor recognized a payment. It does not execute a transfer, and it does not make SubSense an autonomous payment agent.
AI helps explain-it does not take control
Optional OpenRouter assistance can answer questions, explain selected subscriptions, plan renewals, or search selected context semantically.
It does not replace the deterministic recommendation rules or edit subscriptions on its own.
Each request requires explicit sharing consent. The app sends the question and selected, validated context rather than the entire workspace. Wallet identifiers, credentials, payment evidence, and remote receipts are excluded from that context.
Routing is limited to allowlisted free models with zero price caps and no paid fallback. Strict privacy routing is the default. Some free-model endpoints may not satisfy it; the app does not silently weaken the policy to make a request succeed.
Free availability, quota, and provider policies can change. Those are real limitations, not details to hide behind an “AI-powered” label.
The wallet-switch bug that made recovery better
One of the most useful review cases was switching wallets A → B → A while requests were in flight.
Comparing only the current wallet address is not enough. An old request from A can finish after the user returns to A, making stale data look current.
The API client now captures a wallet generation alongside the expected address and rejects the result if either changed:
// Capture these before sending the request.
const expected = walletAddress;
const generation = walletGeneration;
// Check again before applying its response.
if (expected !== walletAddress || generation !== walletGeneration) {
throw new Error('Wallet changed during the request. No result was applied.');
}
Wallet-scoped workspace keys were also aligned with the same address normalization used by login and the vault. Short, padded, and mixed-case representations should identify the same workspace-not accidentally create different ones.
In the browser fixture, wallet B showed neither A's subscriptions nor its vault prompt. Returning to A restored its original profile and reason with the vault still locked. A wrong password preserved the ciphertext; the correct password unlocked it without rewriting it.
What is verified-and what is not yet
The latest local verification passed 90 automated tests, TypeScript checking, a production build, and an isolated production HTTP smoke test. All eight screens were checked at 390, 768, and 1440 CSS pixels in both themes: 48 geometry checks with no document horizontal overflow.
Those tests use generated unfunded wallets, local servers, and mocked provider responses. They verify application behaviour, not real Mainnet integration success.
At the time of writing, the live evidence still outstanding includes:
- Signed login using the intended real wallet extension.
- An authenticated Tatum Mainnet read.
- A real completed Walrus write and recall after reconnecting.
- The hackathon's required 10+ completed Mainnet memories.
- Real-key AI inference and the hosted demo.
Two Vite/esbuild dependency audit findings also remain-one moderate and one high. Public demos should serve built assets rather than expose the development server, but that is not a substitute for fixing the dependencies or conducting a security review.
I want the evidence boundary to be visible: a passing mock test and a genuine completed Mainnet memory are different things.
What I learned
The biggest lesson was that a memory feature is only useful when it changes a visible product decision.
“We stored some text” is not the same as “the next renewal recommendation understood why this tool mattered.”
The second lesson was that remembering and recovering are different problems. Local workspace storage, an encrypted credential vault, and remote decision memory each need their own lifecycle and recovery story.
SubSense began from a friend's SpendGuard scaffold. That foundation deserves attribution; the work described here focuses on SubSense's decision-feedback experience, wallet-specific recovery, provider boundaries, and verification. The visual direction was inspired by Goodface's Emerald24 case study, without including its artwork or branding.
The goal is not a finance app that makes more decisions for you. It is one that helps your next decision begin with the context you already worked out.
What would you want a subscription manager to remember: usage, budget trade-offs, work dependencies-or something else?
Links
- SubSense source code
- Total Recall — Tatum × Walrus Memory Hackathon
- Tatum documentation
- Walrus Memory documentation


Top comments (0)