Money that works like a file.
Why we built dotMoney, how it works, and what’s underneath it.
Building or reviewing it? The technical details are below ↓The idea
We wanted to see how close a file could get to cash. Something you could hand to someone, put on a USB stick, or send through whatever app you already use.
It also gave us a way to try our own docs and products across Mac, iPhone and the web. We wanted to build something different that used several parts of Lightspark together.
dotMoney is the prototype. Everything runs on test funds with no monetary value.
How it works
Choose an amount and dotMoney shows it on a money file. Add a note if you want, then send it. When someone accepts, the money moves into their dotMoney balance.
You can send the same money file three ways:
| Send it as | How the other person receives it |
|---|---|
| A share link | Opens in a browser. No app required. |
| A .money file | Opens in dotMoney on a Mac or iPhone. Send it by AirDrop, attach it to a message, or save it to a drive. |
| Hand over | Pass it directly to a nearby iPhone with dotMoney open. The recipient can accept it or send it back. |
You don’t choose between a file and a link before making the money file. Make it first, then choose how to send it.
- It can only be claimed once. Copying a file or forwarding a link doesn’t create more money. The first successful claim receives the funds. Other copies can no longer claim them.
- You can cancel before it’s accepted. Creating a money file takes the amount out of your available balance. Cancelling returns it. If you close the composer without sending, the app returns the amount automatically.
- Unclaimed money files expire. The money file shows how much time remains. After expiry, the sender’s app recovers any unclaimed funds when it next runs.
- The details stay with it. The note, optional sender name and expiry can be changed until the money file first leaves your device. After that they’re fixed, so the recipient sees the details you sent. The same eight-character code appears on the money file, in activity and on the claim page, making it possible to distinguish two money files for the same amount.
- Files are saved where you choose. dotMoney creates a
.moneyfile when you drag or save one. It doesn’t automatically put one on your Desktop. It warns when you choose a folder it recognizes as synced.
Getting the money out
Transfers between dotMoney balances use Spark. We’re also using Lightspark Grid to build cash-out options for banks, Cash App and Lightning wallets.
Those payout flows run in Grid’s sandbox and are still incomplete. The real Grid legs run from the Mac and the web; on the iPhone, the Cash App and bank flows run as simulations, and the prototype’s current test token does not complete every Grid payout. The technical details below explain those limits.
Privacy
You don’t need to create a profile or add your name to send or receive within dotMoney. If you include a name on a money file, the recipient sees it.
That doesn’t make the exchange anonymous. AirDrop may show your device name. A message or email may identify you to the recipient and the service carrying it. A filename or preview can also reveal the amount.
Hand over sends the money file between nearby phones without exchanging names or phone numbers. The money file is still registered with our service so its share link works.
| Who | What they can see or access |
|---|---|
| The app or service carrying the file or link | Depends on how you send it. Names, addresses, timestamps and visible file details may be available. |
| The dotMoney service | Registered money files’ amounts, expiry, notes, optional names and claim records. It can access the funds in registered, unclaimed money files and balances held in the browser. |
| The nearby phone | The money file you send. The handover protocol does not send your phone number, phone model or a stable device identifier. Temporary discovery information is visible on the local network. |
| Lightspark Grid | The information needed for a payout. The bank flow collects identity and bank details. The prototype’s Cash App and Lightning flows use placeholder customer records in the sandbox. |
| iCloud | An encrypted wallet backup if you enable that option. A .money file saved to an iCloud Drive folder also syncs, and that file is not encrypted by dotMoney. |
The service also keeps records to limit requests for free test funds. The technical section describes those separately from money file transfers.
Current limitations
- Anyone who gets an unclaimed file or link may be able to take the funds. A forwarded attachment, a synced copy or a lost drive can expose them. You can cancel while the funds remain unclaimed, but cancellation can lose a race with someone accepting. Files are not password-protected.
- The browser depends on our service. We hold the funds accepted into a browser balance and can access registered money files while they’re unclaimed. This is a trust requirement of the prototype.
- Hand over needs both apps open. Both phones need to be nearby with dotMoney open on Wallet. The sender connects only when exactly one receiver is visible and says so when there are more. The handshake’s check word is not shown in this build; the current flow is intended for testing between people standing together.
- Carrying a file offline doesn’t make the payment offline. A file can sit on a drive without a connection. Accepting its funds still requires access to Spark.
If both people already use the same payments app, sending there may be simpler. What we’re exploring here is the ability to send through files, links and nearby devices without making both people use the same delivery method.
How we built it
We built the Mac and iPhone apps and the web service with an AI coding agent, using our docs and APIs. We described the interactions, tried the builds, and sent back screenshots and recordings of what needed work.
We were surprised by how quickly we had working software moving test funds. Testing across devices took considerably more effort. A successful transfer on one pair of phones wasn’t enough. We had to keep checking different networks, interrupted connections, and what happened when either person left the screen.
A few decisions changed along the way. We removed the initial file-or-link choice, stopped saving new files to the Desktop, and switched from displaying converted sats to using a dollar-denominated test token. Each change made the basic experience easier to understand.
Technical details
The money file is the visual representation of an amount being sent. Underneath it, Spark wallets hold the funds and keys authorize transfers.
The prototype uses Spark’s regtest network and a test token with USDB’s ticker and six decimal places. This stand-in token has no monetary value and is distinct from the USDB token used by Grid’s sandbox. Reviewed against the build of September 11, 2026.
The stack
| Part | Implementation |
|---|---|
| Mac app | Swift and SwiftUI, with AppKit for desktop integration. A bundled Node process runs the Spark JavaScript SDK. |
| iPhone app | Shared Swift sources, with phone-specific navigation, sharing and nearby transfer. Uses the Breez SDK for Spark in the app process. |
| Key storage | CryptoKit, the Security framework and Secure Enclave, with Touch ID or Face ID for access to the user’s wallet. |
| Claim service | Node.js using node:http, hosted on Fly.io. Handles browser claims, browser balances, test-fund requests and web payout flows. |
| Web interface | JavaScript and canvas. Recipients can accept links without installing the native app. |
| Transfers | Spark token transfers between wallets. |
| Cash-out | Lightspark Grid quotes and payout flows in sandbox. |
The native apps share much of their interface and transfer logic. The Mac’s Spark process and the iPhone’s in-process SDK expose the same application-facing operations.
How funds move
The system uses Spark wallets for four purposes:
| Wallet | Purpose |
|---|---|
| User wallet | Holds someone’s balance in the native app. |
| Temporary wallet | Holds the amount attached to one money file until it is accepted or recovered. |
| Browser wallet | Holds funds accepted through a browser. The claim service controls its key. |
| Treasury wallet | Supplies test funds to new wallets. |
Creating a money file. When someone enters an amount and continues, the app creates a temporary wallet and transfers that amount into it. The sender records the wallet information locally so the transfer can be reconciled or recovered.
Sending. The first send fixes the note, optional name and expiry. The app registers the money file with the claim service so the share link can work. If registration is unavailable, the file path can still be used without a working share link.
Accepting in the app. The app reads the temporary wallet’s key from the file and transfers its funds into the recipient’s wallet. The first successful transfer empties the temporary wallet; another copy of the file cannot spend the same funds again.
Accepting in a browser. The claim service performs that transfer into a browser wallet. Further claims from the same browser can add to the same balance.
Cancelling. The sender’s app transfers the remaining funds back into the sender’s wallet and updates the claim service.
Expiry. The claim service rejects expired browser claims. Recovery is performed by the sender’s app when it runs, so the funds may remain in the temporary wallet after the displayed expiry. Expiry does not erase the key from an existing file.
The sender’s app updates file copies it can locate to show that they were accepted, cancelled or expired. It cannot rewrite every copy someone may have forwarded or backed up.
Where the keys live
The user’s main wallet key is protected by the native app’s vault. Temporary wallets currently have different storage requirements:
- A
.moneyfile contains its temporary wallet’s seed in plaintext. - The sender keeps the seed in a local registry so unclaimed funds can be recovered.
- For registered money files, the claim service keeps an encrypted copy so a browser can accept them.
This is why possession of a file matters. Someone who can read its seed can attempt to move the funds without using dotMoney.
Copying a file also leaves the sender with access. A recipient should treat acceptance—the transfer into their own wallet—as the point at which they have received the funds.
Test funds
A hosted treasury supplies test funds to new wallets: $100 once, and a top-up once a day on request. Requests are limited by wallet address, network address and daily totals. Stored hashes and counts support those limits.
The development Mac also has a local treasury. These are prototype conveniences, not a source of funds for a real-money version. A production wallet would need to be funded by its user.
Earlier builds used sats and a fixed conversion for dollar display. The service retains compatibility with those files and browser balances. New money files use the dollar-denominated test token.
Grid payouts
A transfer to another dotMoney wallet is a Spark token transfer. Other destinations use a Grid quote.
For a bank payout, the flow creates the required customer and destination account, requests a quote, and funds the Spark deposit address specified by that quote. The app or service then follows the quote until Grid reports the resulting transaction.
For Cash App and Lightning, the flow obtains a Lightning invoice and requests a quote for that destination. The prototype estimates the invoice size using a small probe quote, then checks the actual quote against the available balance.
There are several unfinished parts:
- Token compatibility. The prototype currently sends a stand-in token that Grid does not recognize as its sandbox USDB. A bank quote can remain pending rather than complete.
- Customer records. The Cash App and Lightning paths use generated placeholder customer records. That sandbox shortcut needs an appropriate production design.
- Incomplete payouts. Funding a quote and receiving a completed Grid transaction are separate steps. A funded payout can remain unresolved; the prototype does not yet have a complete compensating-transfer path.
- Remaining balance. The Lightning invoice calculation includes headroom and may leave funds behind. The browser does not yet offer a complete flow for that remainder.
- Platform availability. Grid’s credentials live on the Mac and the service, so the real Grid legs run only from the Mac app and the browser. On the iPhone, Cash App and bank payouts run as labelled simulations that return the test funds to the treasury; Lightning there is limited to another dotMoney wallet’s address.
The interface should report a pending payout as pending. Paying the deposit address is not, by itself, proof that the recipient has been paid.
The claim service
The claim service connects native money files with browser access. Its main responsibilities are registration, acceptance, status, browser balances and payouts.
| Operation | Required information |
|---|---|
| Register a money file | App sender key, money file details and proof of funds. |
| Accept a claim | The claim token contained in the share link. |
| Cancel or settle | The app sender key and the money file’s own seed. |
| Read claim status | The claim token; receiver-specific information requires the receiver credential. |
| Quote or pay out a browser balance | The browser’s receiver credential and the relevant claim checks. |
The app sender key ships inside the download and must be treated as public. Registration therefore verifies the advertised amount against the wallet’s actual token balance. A supplied label is not sufficient evidence of funds.
Claim tokens and browser receiver identifiers are random credentials. The receiver identifier is stored in browser localStorage and sent to the service in a header. It is the credential for that browser balance: clearing site data can lose access, and leaking the identifier can expose the funds.
The service serializes operations on the same wallet, writes its records atomically, and encrypts stored seeds and sensitive payout fields with AES-256-GCM. Requests have rate limits, body-size limits and storage caps.
External Lightning callbacks must use HTTPS and cannot resolve to local or private-network addresses. Invoices are checked against the expected amount and network before payment.
Native apps
The native wallet vault uses Secure Enclave-backed key protection where available. The Mac has a keychain fallback for devices without an Enclave. Wallet backup through iCloud Keychain is optional.
The Mac app locks its wallet when the screen locks or the Mac sleeps. Its bundled Spark process receives keys when performing wallet operations. Release builds restrict that process to the signed application bundle and the configured claim host.
The Mac app is currently unsandboxed because it launches a Node child process. It is Developer ID signed and notarized. Sandboxing would require a different process boundary.
Both platforms validate incoming files and route files and supported links into the receiving flow. Quick Look previews and thumbnails display the amount before the file is opened. On iPhone, the preview extension uses the app’s money file view so the preview and receiving screen agree.
The eight-character code printed on a money file is an identifier. It is not the claim token and cannot authorize a transfer.
Nearby handover
Hand over uses Bonjour discovery and Network.framework. The connection can use peer-to-peer Wi-Fi or a shared network. The sender proceeds only when one receiver is visible; multiple receivers stop the flow rather than making the app choose.
Each session uses fresh encryption keys. Both screens derive the same check word from the handshake; it is kept in the protocol but not shown in the current build. The discovery information does not include a person’s name, phone number or stable device identifier.
On screen, the connection is a field of grains. The sender’s card stands in it; when the other phone is found the two fields join through a neck, and once they have, a flick or a slingshot pull sends the card through it. Nothing sends but that gesture. The receiver watches the card arrive and can accept it or flick it back up the same neck, and the sender can send it again on the same connection. Accepting runs the real claim first; the grains fall only after it succeeds, and the card goes into the balance through a slit beside the figure. Sound and haptics are one score with the motion.
The money file travels between the phones. Registration with the claim service is separate and still exposes the registered money file’s key and details to that service.
Delivery and acceptance are separate events. The receiver first saves an encrypted receipt and reads it back before acknowledging delivery. Accepting then moves the funds through Spark.
If the connection drops, the phones authenticate a resumed session before exchanging updates. A lost acceptance acknowledgement can also be reconciled against Spark. An uncertain delivery is not automatically refunded: the money file may already be on the other phone.
The current one-word check is limited. Nearby discovery can be imitated, and an open receiving screen can be interrupted by another nearby app. A real-money version needs stronger confirmation and handling of unwanted connections.
Decisions and recovery
Several implementation details came directly from testing:
- Discovery must remain active until connection is ready. Stopping the browser as soon as it found another phone could prevent address resolution on the peer-to-peer path.
- An idle connection can disappear. The handover connection needed more frequent keepalive traffic to remain available through the interaction.
- A timeout does not prove that funds stayed put. A transfer can complete while its response is lost. Recovery needs a durable record and reconciliation before deciding whether money can be returned.
- The sender’s status cannot depend on a particular screen staying open. Acceptance needs to update the transfer record and activity even if the sender has already returned to their balance.
These cases needed repeated tests on physical phones. Home Wi-Fi, office Wi-Fi, hotspots and peer-to-peer connections did not behave identically.
Visual details
The money file is a square with continuous corners: the corner is 0.15 of the side, the inset 0.115, and the content area inside the inset is concentric with the face. The mark sits top left, the currency top right, the sender’s name and the note on one line above the figure, and the figure on the bottom margin. A live file says nothing about its state; a claimed, expired or cancelled one goes one grey with a grey figure and the word under the currency. Every size, from the Finder icon to the opened file, is the same drawing scaled; the claim page and the link preview draw the same square in ink.
The wallet is engraved into view while it is being created. The native animation follows the letter shapes and plans its particles before playback. The web version derives its paths from rendered text so it can use the page’s actual type and layout.
The animation runs from one shared clock. Engraving, particles, sound and the transition to the live wallet stay aligned without depending on a chain of timed waits.
We also explored fluid smoke for nearby discovery. In the published implementation described here, handover uses a simpler blue edge glow; the fluid effect remains a design study because of its cost on real phones.
Remaining technical risks
The prototype is useful for trying the interaction, but several parts need further work before real money:
| Area | Remaining issue |
|---|---|
| File protection | Files contain plaintext seeds. Copies can expose unclaimed funds. |
| Local recovery records | The sender’s registry contains temporary-wallet seeds in plaintext, separate from the protected main wallet. |
| Service custody | The service can access registered money files and browser balances. Encryption at rest does not remove that access. |
| Browser recovery | Access depends on a credential in browser storage. There is no complete account-recovery flow. |
| Service durability | One machine and an unreplicated volume leave browser balances vulnerable to data loss. Sender records can help recover unclaimed funds, but not replace every service record. |
| Registration abuse | A public app key and proof-of-funds checks still allow funded claims to consume service resources. |
| Payout recovery | A funded Grid quote may remain unresolved without a compensating transfer. |
| Nearby authentication | The current check and passive receiving behavior need stronger protection for real-money use. |
| Mac isolation | The app is unsandboxed and passes wallet keys to its bundled Node process. |
| Web security | The current content-security policy allows inline scripts and styles; escaping and other controls remain important. |
File encryption, different browser custody models and stronger nearby confirmation are possible directions. They are not completed features.
Testing and operations
The service runs on Fly.io and deploys itself from the repository on each push to the main branch. Service deployment and native-app distribution are separate: the Mac app is distributed as a notarized download that is published to the service’s storage without a deploy, and the iPhone app through TestFlight.
Private service credentials are configured outside the application image. The shared sender key embedded in the app is treated separately as a public value.
Automated checks cover claim funding and acceptance, file validation, key storage, malformed handover messages, interrupted connections, lost acknowledgements and recovery. Cross-platform checks exercise files and links between the Mac, iPhone and browser. The service’s own tests run before every deploy.
Physical-device testing remains a separate requirement. Simulator and protocol tests cannot establish that discovery and transfer are dependable on every network. The published technical notes still identify the full physical-device network matrix as unfinished.
Monitoring currently consists of a service health check and logs. Broader operational monitoring and recovery remain work to do.
Back to the top ↑Further reading: the Spark documentation and the Lightspark Grid documentation. Or try dotMoney.