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 asHow the other person receives it
A share linkOpens in a browser. No app required.
A .money fileOpens in dotMoney on a Mac or iPhone. Send it by AirDrop, attach it to a message, or save it to a drive.
Hand overPass it directly to a nearby iPhone with dotMoney open. The recipient can accept it or send it back.
One money file, three ways to send it Your wallet funds one money file with its own small wallet and one link. The same money file leaves as a share link, as the file itself, or straight to a nearby iPhone. A link opens in a browser; a file and a hand over open in dotMoney. The other person accepts it in dotMoney or a browser. Cash-out to a bank, Cash App or Lightning is still being built. You can cancel until it’s accepted. Unclaimed, it expires and the sender’s app recovers the funds. One money file, three ways to send it Your wallet Choose an amount, then Continue. USD Geoff · Lunch $2.40 One money file. One link. Its own small wallet. Share link Chat, email, anywhere. Opens in a browser. No app. .money file AirDrop, a message, a drive. Opens in dotMoney. Hand over To the iPhone beside you. dotMoney open on both. They accept it in dotMoney or a browser. Cash-out to a bank, Cash App or Lightning is still being built. Cancel it before it’s accepted. Unclaimed, it expires and your app recovers the funds. One money file, three ways to send it: a share link, the file itself, or a nearby phone. They accept it in dotMoney or a browser. Cash-out options are still being built. Cancel before it’s accepted; unclaimed, it expires and your app recovers the funds. One money file, three ways to send it Your wallet An amount, then Continue. USD Geoff · Lunch $2.40 One money file. Its own wallet. Share link Chat, email, anywhere. Opens in a browser. .money file AirDrop, message, drive. Opens in dotMoney. Hand over The iPhone beside you. dotMoney open on both. They accept in dotMoney or a browser. Cash-out options are still being built. Cancel it before it’s accepted. Unclaimed, it expires and your app recovers the funds.

You don’t choose between a file and a link before making the money file. Make it first, then choose how to send it.

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.

WhoWhat they can see or access
The app or service carrying the file or linkDepends on how you send it. Names, addresses, timestamps and visible file details may be available.
The dotMoney serviceRegistered 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 phoneThe 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 GridThe 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.
iCloudAn 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

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.

For engineers, PMs, and security reviewers

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

PartImplementation
Mac appSwift and SwiftUI, with AppKit for desktop integration. A bundled Node process runs the Spark JavaScript SDK.
iPhone appShared Swift sources, with phone-specific navigation, sharing and nearby transfer. Uses the Breez SDK for Spark in the app process.
Key storageCryptoKit, the Security framework and Secure Enclave, with Touch ID or Face ID for access to the user’s wallet.
Claim serviceNode.js using node:http, hosted on Fly.io. Handles browser claims, browser balances, test-fund requests and web payout flows.
Web interfaceJavaScript and canvas. Recipients can accept links without installing the native app.
TransfersSpark token transfers between wallets.
Cash-outLightspark 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:

WalletPurpose
User walletHolds someone’s balance in the native app.
Temporary walletHolds the amount attached to one money file until it is accepted or recovered.
Browser walletHolds funds accepted through a browser. The claim service controls its key.
Treasury walletSupplies 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 life of a money file Continue mints the money file: a temporary wallet with the money in it. The first exit makes the money file Out, locks its note, name and expiry, and registers it with the claim service. A money file that never left is swept back when the composer closes, or at the next launch if the app died. An Out money file is accepted, cancelled before acceptance, or expires; expired money comes back when the sender's app next runs. A money file handed to a phone can also be sent back unspent: it stays Out and registered, to send again or cancel. Accepted, cancelled and expired money files turn any .money copy the app can find into a receipt. The life of a money file Minted Continue makes a temporary wallet and moves the money in. left = false first exit Out Note, name and expiry lock. Registered with the claim service.* left = true Never left Back in your wallet the moment you close the composer. If the app died first, swept back at the next launch. Nobody can have it. Accepted The receiver sweeps the money. The money file is spent. Cancelled You, any time before it’s accepted. The money returns to your wallet. Expired 24 hours. Back in your wallet when your app next runs. Sent back Hand over only. It comes back to your hand unspent, still Out and still registered. Send it again, or cancel it. Accepted, cancelled or expired, any .money copy the app can find becomes a receipt. * If the service is unreachable it still leaves as a file, just without a link. The life of a money file: Minted, then Out at its first exit. Accepted money files are spent; cancelled and expired money files come back to your wallet, expired ones when your app next runs. Sent back is unspent and still Out. The life of a money file Minted Continue makes the wallet, money in. left = false Never left Swept back when the composer closes, or at the next launch if the app died. The first exit locks note, name and expiry. Out Registered with the claim service.* left = true Accepted The receiver sweeps the money. The money file is spent. Cancelled You, any time before it’s accepted. The money returns to your wallet. Expired 24 hours. Back in your wallet when your app next runs. Sent back Hand over only. Comes back to your hand unspent, still Out. Send it again, or cancel it. Accepted, cancelled or expired, any .money copy the app can find becomes a receipt. * Service unreachable: still leaves as a 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:

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:

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.

OperationRequired information
Register a money fileApp sender key, money file details and proof of funds.
Accept a claimThe claim token contained in the share link.
Cancel or settleThe app sender key and the money file’s own seed.
Read claim statusThe claim token; receiver-specific information requires the receiver credential.
Quote or pay out a browser balanceThe 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:

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:

AreaRemaining issue
File protectionFiles contain plaintext seeds. Copies can expose unclaimed funds.
Local recovery recordsThe sender’s registry contains temporary-wallet seeds in plaintext, separate from the protected main wallet.
Service custodyThe service can access registered money files and browser balances. Encryption at rest does not remove that access.
Browser recoveryAccess depends on a credential in browser storage. There is no complete account-recovery flow.
Service durabilityOne 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 abuseA public app key and proof-of-funds checks still allow funded claims to consume service resources.
Payout recoveryA funded Grid quote may remain unresolved without a compensating transfer.
Nearby authenticationThe current check and passive receiving behavior need stronger protection for real-money use.
Mac isolationThe app is unsandboxed and passes wallet keys to its bundled Node process.
Web securityThe 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.