Work / Cryptoopen brief

Tally Invoices: payment links in USDC and EURC on the Arc network

What it was

A freelancer wants to get paid in stablecoins, but on most networks the client first has to buy a second coin for the fee. Many give up at this step, and the seller then hunts for the transfer in a block explorer. Circle opened a brief for live projects on its new Arc network, where the fee is paid in the same USDC.

What we did

Picked the idea through a council and threw out everything unnecessary. Wrote a 124-line contract with 13 tests, one static page, and a Telegram bot that watches a wallet. Deployed to Arc mainnet, verified the source on Sourcify, made three real payments from our own wallets and submitted the application.

What the client got

A USDC invoice is paid with one signature, the money goes to the seller in the same transaction, the contract balance is always zero. The first live invoice for 0.50 USDC was paid 6 seconds after creation, the bot sent both notifications. From an empty folder to mainnet, three payments and a submitted application took one day.

ClientOpen brief Arc Microgrants, Arc network
My roleIdea selection, smart contract and tests, website, Telegram bot, mainnet deployment, live payments, video, application copy
StatusApplication submitted, under review. The site, contract and bot are running
Result0.50 USDC the first live invoice, paid on mainnet · Open
Rejected: the invoice page took over 5 seconds to open, the viem library was pulled from the external CDN esm.sh. A reconstruction
Rejected: the invoice page took over 5 seconds to open, the viem library was pulled from the external CDN esm.sh. A reconstruction
Accepted: viem bundled into one file of our own (103 KB compressed), the page opens in 0.9-1.1 s. The first live invoice for 0.50 USDC
Accepted: viem bundled into one file of our own (103 KB compressed), the page opens in 0.9-1.1 s. The first live invoice for 0.50 USDC

The task

Circle, the issuer of USDC, opened a brief on DoraHacks: 20 microgrants of 500 USDC each for live projects on its new Arc network. Two conditions: the project is deployed on Arc mainnet and the code is open.

We wanted to test whether we can take a smart contract to production in a day. Not a mockup, but a contract with tests, verified source code and real money that has passed through it. And get a small but finished product for the portfolio: a site, a contract, a bot and proof on the blockchain.

The problem we solved is simple. A designer drew a logo for a cafe in Berlin and wants to receive 25 USDC. The usual path in crypto: send the client a wallet address, explain the network, and the client still has to find a coin for the fee. A client paying for the first time often gives up at this step. On Arc the fee is paid in USDC. If the client has 25 USDC, they can pay a 25 USDC invoice, and that is it.

Why, and what the idea was

The idea was chosen by a council of five AI advisors: a skeptic, an architect, a visionary, an outsider, a pragmatist. Verdict: a payment link service with a neutral name, not “Arc Pay”, because that sounds like a product of Circle itself. Throw out everything unnecessary: a currency exchange, x402, AI, swaps. The skeptic warned then that there would be many payment links among the applications. He was right, more on that below.

Three parts. The home page: amount, currency (USDC or EURC), payment description, a “connect wallet and create” button. The invoice page at the link ?i=number: visible to everyone, even without a wallet, with status, amount, who paid and when, and a link to the transaction. The Telegram bot: the seller sends /watch <address>, and the bot reports when an invoice is created for that address and when it is paid. The bot only reads the network, it holds no keys.

Every action on Dmytro’s behalf on DoraHacks (posting a question, submitting the application, accepting the terms) was done only after his direct “yes”.

How it went

When What happened
Morning Council on the brief. Verdict: payment links, a neutral name, nothing extra
Morning The contract and 13 tests, the site, the bot. A clean wallet for deployment, waiting for funds
08:50 Registration on DoraHacks, a Q&A question about payouts to Ukraine
09:50 Wallet funded with 2.99 USDC. Deployment for 0.034 USDC, invoice #1 created and paid, code verified on Sourcify. Site on Vercel, repository made public
09:51 Bot started on the server
10:00 An automated security review found an unescaped payment description in the bot’s message. Fixed
10:05 End-to-end check: invoice #2 created and paid. The bot was silent at first, fixed it, both notifications arrived
10:20 Application on DoraHacks: profile, description, a 10-question form. Submitted on Dmytro’s word
12:40 Research: 334 applications for 20 places. Follow-up work: a live payment in EURC, a 61-second video
12:55 The page sped up from 5+ to 1 second, the video and EURC added to the application

5 commits in total, about 900 lines of code including tests and scripts. Working days: one.

Tools and method

The contract TallyInvoices.sol, 124 lines of Solidity. An invoice has three states: open, paid, cancelled. Only the seller can cancel. The money does not linger on the contract: it goes to the seller in the same transaction. The invoice state changes before the transfer, so a reentrant call finds the invoice already paid. A token address with no code is rejected, otherwise an invoice could be “paid” by a call that transfers nothing. A wrong amount, a repeat payment and an overlong description revert.

Tests in Foundry, 13 of them: among them a reentrancy attack, a seller who refuses to accept the money, and a fuzz test of the amount over 256 runs. The source code is verified on Sourcify with an exact bytecode match.

The site: one static page on Vercel, viem bundled into one file. Network reads go through our own proxy /api/rpc, which allows only 10 read methods and no more than 20 calls per batch. Writing through it is impossible. The bot is in Python with web3 and SQLite for subscriptions, polls the network every 3 seconds, runs as a service on our server.

Keys: the contract was deployed from a separate clean wallet created for this project. Transactions are signed locally, keys sit in owner-only files and never reach the repository. The same for the bot token. The service holds nobody’s money.

Result

Three real payments on Arc mainnet. Invoice #1 for 0.50 USDC was created and paid from another wallet 6 seconds apart, the seller got exactly +0.50, the contract holds 0. Invoice #2 for 0.25 USDC: the bot sent both notifications, on creation and on payment. Invoice #3 for 0.50 EURC was paid with two signatures (an approval and the payment), because EURC on Arc is a regular ERC-20 token. The seller got exactly +0.50 EURC.

The buyer’s fee for one payment is about 0.0014 USDC, deploying the contract cost 0.034 USDC. The hashes of all transactions are in the repository README. A paid invoice opens without a wallet: tally-invoices.vercel.app/?i=1.

Speed: the invoice page in a clean browser took over 5 seconds to open; after bundling viem locally, 0.9-1.1 seconds as measured by Playwright.

The weak spots, said plainly. There are no real users, this is a demo with our own payments: the counter on the site showed 3 invoices created, all ours. There is no feature lead over other applications either, the product is deliberately small.

Mistakes and what we took from them

The bot was silent on the first real payment.The new web3 version on the server requires the field "anonymous": false in the contract's event descriptions, without it it crashes with a KeyError. It did not surface in tests, only in the live run. We added the field, the bot caught up on the missed blocks itself and sent both notifications. Lesson: test the bot with a live payment before submitting, not by reading the code.
An unescaped payment description.The seller writes the description themselves, and the bot inserted it into an HTML message as is. Found by an automated security review, fixed in one line. Lesson: any user text that goes into markup gets escaped right away.
Circle blocks Ukrainian IPs.The Arc network's public RPC and part of the documentation answered our Mac with a 403. So reads on the site go through our own proxy on Vercel, and transactions were signed locally and sent through a server outside Ukraine. Lesson: in a project on someone else's infrastructure, the first thing to check is whether it is reachable from our country.
The page took over 5 seconds to load.viem was pulled from the external CDN esm.sh. For a payment page that is too long. We bundled the library into one file of our own (317 KB, 103 KB compressed), and it dropped to about 1 second.
There were far more applications than it seemed.The public counter showed 0, but enumerating through the DoraHacks API found 334 applications for 20 places, about 30 of them about invoices and payment links, one even called "Tally". 37% of applications had a video, ours did not. Within an hour we added EURC payment and a 61-second video. Lesson: look at the others before submitting, not after.
DoraHacks forms.The visual editor cut off links to .vercel.app, we had to switch to markdown and check all 8 links in the preview. Dropdowns stayed invisible and overlapped neighbouring fields, the video field did not accept normal input. Lesson: after a form like this, verify what was saved through the API, not by eye. The description and video were verified with a request to their API.
The Telegram scene in the video was built from the bot's template, not captured from the screen. The notifications themselves arrived for real, but in the video it is a reconstruction. We say so honestly.

Tools

SolidityFoundryArc mainnetUSDC and EURCSourcifyviemVercelPython and web3SQLiteTelegram Bot APIPlaywrightffmpegClaude Code