Inspiration
Nearly one third of sub-Saharan African countries had double-digit inflation in 2024 (IMF, October 2024). The Nigerian naira has lost about 70% of its value against the dollar since June 2023 (Bloomberg). Argentina's prices more than doubled in 2024, with inflation of 117.8% (INDEC, via Buenos Aires Times). Many countries are looking for a stable alternative to their own currency.
They have found one. Stablecoins are 43% of crypto transaction volume in sub-Saharan Africa, and Nigeria ranked second in the world for crypto adoption in 2024 (Chainalysis).
I asked myself how money will move in the next decade. After researching it, the answer is clear: it will be instant, it will be stable, it will be cheap to send, and it will cross borders anywhere in the world without trouble.
The only thing missing is a system that lets customers and merchants use stablecoins the way they already use money. There is no simple way to walk into a shop and pay with one. I set out to build that system.
SolTap is a stablecoin payment system on Solana. It lets anyone in the world pay with a card whose balance is USDC, Circle's stablecoin pegged to the US dollar. It is instant, cheap, stable and safe. I built it so that people can take control of their own money and have financial freedom, no matter where they live, what their government does, what their credit score is, or what their circumstances are.
The problem SolTap solves
Merchants. In most stores a card payment is approved in seconds but paid out two days later. At scale that is a problem: a merchant doing a million dollars a month has tens of thousands of dollars always in transit. Merchants pay fees on every sale, and they are exposed to employees in a position to take money from the register.
Users. Many people do not actually control their own money. They hand it to a bank that decides how much they can withdraw and whether a transfer goes through or gets frozen. Sending money across a border is hard and expensive. And in much of the world, cash loses value just sitting in an account.
What SolTap changes. SolTap uses USDC, a stablecoin that holds its value, and settles every payment on Solana, so payments are not controlled by a bank, they arrive instantly, and they cross borders the same way they cross the street. I built a fraud detection model and put it in front of the payment, so fraud is stopped before the money moves, not after. For the merchant, every sale is a public transaction, so the books are a download with a Solana signature on every line.
What it does
- Open an account, issue a card. Name and birthday open an account with its own Solana wallet. Holding a blank NFC card to the phone registers its factory serial as a card on that account, on chain, with a status the program enforces.
- Load it from your own wallet. Send USDC to the account's wallet from anywhere, then load the card. The wallet signs and the card is credited on chain in one instruction. The cardholder can withdraw, lock or close the card from the phone. Closing a card sends its balance back to the wallet, and deleting the account sweeps the wallet to any address the user chooses.
- Fraud decided before the payment. Every transaction is scored in process in under a millisecond, and the score is carried on chain with the authorization.
- Paid at the tap. USDC moves to the shop in the same transaction as the authorization. Median 888 ms on devnet.
- Declined transactions. The customer sees why in plain words, the shop sees "not charged," and no money moved, so there is nothing to reverse.
- The merchant backend. A kiosk on the counter takes the order, and a screen behind it shows the shop its balance, its sales for the day, what the fraud model kept off the books, and every transaction at the register. The payment log and the tax log download as files with a Solana signature on every line.
- The network dashboard. One screen for the whole network: a live map of transactions, the most recent transactions and their Solana Explorer links, settlement totals, transactions per second, settle time, the fraud model's scores, and why the last transaction was stopped. Every number comes from a chain event or the live service.
Solana
While building this project I realized that Solana is the only platform that can support it. A card network needs four things at once: settlement in about a second, fees too small to notice on a cup of coffee, the capacity to handle hundreds of transactions a second, and a dollar stablecoin to settle in. Solana is the only chain I found that delivers all four. The project is built around it, which is why it is called SolTap. Every card, every shop and every decision lives on chain.
- Low latency. A
paytransaction, which authorizes and settles together, processed in a median 888 ms on devnet, ranging from 543 ms to 1.1 s. The shop is paid before the customer leaves the counter. - Extremely low fees. About 5,000 lamports per transaction, a fraction of a cent. Settling every sale individually is not viable on a chain with dollar-scale fees. On Solana it is.
- Scalability.
authorize_batchpacks 20 authorizations into one transaction: 32,474 compute units, about 1,624 per authorization, in 1,176 bytes. On a local validator it processed 500 transactions a second, 10,000 in total, with zero failures. - USDC. Shops are paid in USDC, Circle's dollar stablecoin, which is a native SPL token on Solana. Every settlement is a Solana transaction on the explorer, and SOL pays the fee.
How I built it
The program (Rust, Anchor)
I built a Solana program in Rust with Anchor that manages the card accounts, the merchant accounts, the issuer's vault, and the flow of USDC between them.
The program has four accounts: Config holds the issuer key, the settlement mint and the decline threshold. Card is keyed by the card's seven-byte serial and holds its status, daily limit, spent today, balance, and last merchant. Merchant holds the shop's category, its USDC account and its settled total. The vault is the token account where every card's USDC sits, owned by the program.
It has nine instructions: init_config, register_card, register_merchant, deposit, withdraw, pay, authorize_batch, settle and set_card_status. The one a purchase calls is pay. It checks the card's status, its daily limit, its balance and the risk score, writes the decision, and if the transaction is approved moves the USDC from the vault to the shop in the same instruction. There is no point where a purchase is approved but unpaid. The issuer is the only signer, the same as on a real card network: the card never signs, the issuer authorizes on its behalf.
The authorization service (Python)
FastAPI. It holds the live-feature model and the WebSocket feed. It builds features from the card's own history, scores them with LightGBM in process, applies the two-merchants rule, and passes the decision to a Node sidecar that holds the issuer key and calls the program through Anchor.
The fraud model
I built a gradient boosted decision tree model using LightGBM in Python. It is 300 small trees, each at most four levels deep with eight leaves. Each tree asks a few yes or no questions about a transaction, and the answers from all 300 add up to a probability that the transaction is fraud. If that probability is 0.80 or higher, the transaction is stopped before any money moves.
I trained the model on the ULB credit card fraud dataset from Kaggle: 284,807 real card transactions from European cardholders over two days in September 2013. 492 of them are fraud, about 0.17%.
I split the data by time, not at random. The model trains on the first 80% of transactions and is tested on the 20% that happened after. A random split lets the model see the future and gives an inflated score. Because fraud is so rare, I weighted each fraud row about 23 times a normal row so the model cannot score well by calling everything legitimate. Default settings scored 0.85 ROC-AUC on the time split. Shallow trees with heavy regularization scored 0.986 ROC-AUC and 0.79 PR-AUC. One row scores in 0.04 ms.
The score is the sum of the trees, squashed into a probability between 0 and 1:
$$p(\text{fraud}\mid x)=\sigma!\left(\sum_{t=1}^{300} f_t(x)\right),\qquad \sigma(z)=\frac{1}{1+e^{-z}}$$
The Kaggle features are anonymized bank data that a terminal does not have, so the model the terminal calls is a second one, trained the same way, on eight signals the service computes from the card's own history at the moment of the transaction:
$$x=\big(\text{amount},\ \text{hour},\ \text{taps in 60 s},\ \text{two shops in 60 s},\ \text{new card},\ \tfrac{\text{amount}}{\text{card's usual}},\ \text{km from usual},\ \text{merchant risk}\big)$$
The service builds those eight numbers, scores them in process, and sends the score to the program with the authorization. The program refuses the transaction if the score is 0.80 or above and checks its own rules in the same instruction:
$$\text{approve}\iff p<0.80\ \wedge\ \neg\,\text{two shops in 60 s}\ \wedge\ \text{balance}\ge\text{amount}\ \wedge\ \text{status}=\text{Active}\ \wedge\ \text{spent today}+\text{amount}\le\text{limit}$$
Every stop comes with a reason. The score breaks down into one contribution per signal, and the two or three largest are shown to the customer in plain words, for example "card used at two places within a minute."
On the dashboard:
- ROC-AUC 0.986 and PR-AUC 0.79. ROC-AUC measures how well the model ranks fraud above legitimate transactions. 1.0 is perfect, 0.5 is a coin flip. PR-AUC measures how often a flagged transaction really is fraud, which is the harder number when fraud is rare.
- Risk scores. Every transaction plotted by amount and score, with the 0.80 threshold drawn across.
- Why the last transaction was stopped. One bar per signal showing how much each one pushed the score over the threshold.
- Stopped by rules, stopped by the model. How many stops the velocity rule alone caught, next to how many only the model caught.
- Kept from fraud. The dollars stopped before payment.
The hardware
I used blank NTAG215 NFC cards as the payment cards and an ACS ACR1252U USB reader as the payment terminal. The reader sends the service the card's factory serial and the shop's id. The price comes from the register.
I designed the card skins myself, front and back in a pink and a black version, and printed and applied them at the Georgia Tech makerspace.
The dashboard (Next.js)
The network dashboard displays a map of live SolTap transactions and a feed of each transaction with its details and its transaction ID on the blockchain. It is also where the fraud model is monitored: the risk score of every transaction, and for each one it stops, which signals caused it and what kind of fraud it was.
The kiosk. I built a self-service kiosk for a local coffee shop in Atlanta, Land of a Thousand Hills, and scraped their actual menu items, prices and photos to make it as realistic as possible. The customer builds the order, adds a tip, presses pay and taps the card. The receipt shows the settlement time and the Solana transaction as a QR code.
The merchant screen. Behind the kiosk, the merchant can see their balance, every transaction for the day, and every receipt, and download the payment log and the tax log with a Solana signature on each line.
Challenges I ran into
- Getting a physical card tap onto Solana. The reader only gives you the card's serial number. I had to build a listener for the reader, a service that makes the decision, and a sidecar that holds the issuer key and calls the program, and get all three to hand off in under a second. That took caching the blockhash, skipping preflight, and reading the decision back out of the transaction so the screen shows what actually happened on chain.
- Putting a machine learning model inside a payment that settles on chain. The model has to run before the money moves, in the same path as the transaction, and its decision has to be enforced by the program, not just by my server. I trained a second model on the signals a terminal actually has, scored it in process, and carried the score into the
payinstruction where the program checks it against its own threshold. - Connecting the phone app, the kiosk, the service and Solana into one system. The phone opens the account and issues the card, the kiosk rings up the order, the service matches the tap to that order and decides, and Solana settles it. All four have to agree in real time, so I used one WebSocket broadcast to feed every screen from the same on-chain event. The hard part was the failure cases, like keeping the kiosk listening after a declined card so the next card can still pay the same order.
Accomplishments that I'm proud of
- A working point-of-sale system that pays the shop in USDC on Solana in under a second. Order on the kiosk, tap the card, and the authorization and the settlement are one transaction on the explorer.
- A machine learning fraud model I trained from scratch, inside the Solana payment. I did the feature selection, trained it on real card fraud data, and reached 0.986 ROC-AUC and 0.79 PR-AUC on a time-ordered holdout. It scores every transaction before any money moves, and its decision is carried in the program's instruction, so a stopped transaction is on chain with the reason next to it.
- The physical product. I designed the logo and the card skins, and learned at the Georgia Tech makerspace how to print and apply the card skin through lots of trial and error.
What I learned
- How card fraud works. Stolen cards get used fast, card testers run small charges in many places, and a cloned card shows up at two shops at once. Training on real fraud data showed me which signals matter, and that a random train/test split makes a model look better than it is.
- Why stablecoins matter. If your currency lost seventy percent in two years, a dollar that stays a dollar is a savings account. People already hold them. The hard part is spending them at a shop.
- Make the fraud decision before the payment. Card networks hold money for days so they can reverse mistakes. If you decide first and pay second, most of that hold has no reason to exist.
- How to build a Solana program. Accounts and PDAs, a token vault the program signs for, calling into the token program, and that you run out of transaction size before you run out of compute.
- How NFC cards work. An NTAG215 has a factory serial that cannot be changed and a few hundred bytes of storage. I use the serial as the card's identity and keep the balance on chain, so there is nothing on the card to steal.
- What a point-of-sale system does behind the counter. The register sets the price, the reader identifies the card, the service puts the two together, and the receipt is the ledger entry.
- Building an iPhone app with Core NFC. The phone reads a blank card's serial to issue it to an account, and the same app holds the account, its wallet and every purchase.
What's next for SolTap
- Any stablecoin, any token. Right now the card holds USDC and the shop is paid in USDC. The program does not care which token it is, so next I want to let the cardholder hold whichever stablecoin they trust (USDT, PYUSD, EURC, or a local one) and let the shop pick the token it gets paid in, with the swap done on Solana at the moment of the transaction. The customer taps once and the shop gets its own token.
- A stablecoin credit card. Right now the card spends money you already loaded. Next is a credit line in USDC: the issuer sets a limit, the program enforces it the same way it enforces the daily limit today, and repayment is a transfer back to the vault. The fraud model's signals are the start of the underwriting.
- Family accounts. An account can already hold more than one card. Next is one account with a card for each family member, each with its own limit, any of them freezable from the phone, and every purchase in one place. A parent loads a student's card from their own wallet and the student taps.
Built With
- anchor
- blockchain
- core-nfc
- fastapi
- leaflet.js
- lightgbm
- next.js
- python
- rust
- solana
- solana-pay
- swift
- tailwind
- typescript
- usdc