Search projects, hackathons, sponsors and tech

    ← All projects

    PDFi +

    A PDF invoice that pays itself, only to the vault its ENS name publishes, and sells on 1inch Aqua

    Demo

    About

    PDFi Plus is a PDF invoice that sends its own payment. It continues PDFi, which we built at ETHGlobal New York 2025: a PDF with form buttons that talks to a backend holding Coinbase CDP wallets. At Tokyo we rebuilt the invoice itself as a 1inch SwapVM order, checked the payee against ENSv2, and made unpaid invoices sellable through 1inch Aqua. The problem is invoice fraud. An attacker sends a copy of a real invoice with their own bank details and the accounts payable clerk pays it. The FBI's IC3 counted about $2.77B in business email compromise losses in 2024. It works because nothing links the invoice to the account printed on it. In PDFi Plus the merchant's wallet signs the invoice as an EIP-712 SwapVM order, and the money can only go to a vault created for that merchant and invoice number. At settlement our router resolves two ENSv2 names on-chain: acme-demo.eth has to point at the wallet that signed the order, and 1042.invoices.acme-demo.eth has to point at the vault. Edit the PDF and the signature breaks. Sign a new order with your own wallet and the ENS check reverts the payment. The clerk never compares account numbers, because the chain does it at the moment of payment. Paying is one step. In Adobe Acrobat the PDF has a Pay panel: the payer types their PDFi username and password, the PDF submits the form to our backend, and the backend sends the transaction from the payer's CDP server wallet. The payer needs no browser wallet and no ETH for gas. Every PDF also prints a pay link and QR code, so any viewer can pay on the web with a PDFi account or their own wallet. The payment terms live in the order: on a 2/10 net 30 invoice, paying in the first 10 days costs $980 instead of $1,000, and paying late adds 0.1% a day up to 3%, all computed on-chain when the payment lands. An invoice can be paid once. Unpaid invoices are also assets. Each invoice vault has a ClaimToken whose supply equals the face value. A financier ships one Aqua strategy that holds a single USDC balance and lists the claims of every invoice they are willing to buy. When a merchant clicks "Get paid now", the app quotes every strategy on-chain and sells to the best bid. When the customer pays, the financier redeems the full face value from the vault. The price comes from our discount-curve opcode: it rises toward face value as the due date gets closer and never exceeds what the customer would pay early. Everything runs on Ethereum Sepolia with demo USDC. The live demo has paid and factored invoices you can check on Etherscan.

    How it's made

    Contracts (Foundry, solc 0.8.30, via_ir). We forked 1inch SwapVM v1.0.2 and added three opcodes to the router's instruction table. Opcode 34, _paymentTermsXD, computes the amount due from the invoice terms and the block timestamp and records the payment, so a second fill reverts with AlreadyPaid. Opcode 35, _ensPayeeXD, takes DNS-encoded names from the signed order, finds their resolvers through ENSv2's UniversalResolverV2 and requires addr(merchant name) == order maker, addr(invoice name) == vault, and vault == VaultFactory.vaultOf(maker, invoiceId). Opcode 36, _discountCurveXD, prices ClaimTokens in Aqua mode as min(face x (1 - d), face x e^(-r x t)) with solady's expWad. One router handles both signed-order mode (paying) and Aqua mode (factoring). Opcodes can't read the order's traits or receiver, so the vault binding had to come from CREATE2 in VaultFactory. Fitting three extra opcodes under the EIP-170 size limit took via_ir and optimizer tuning (runs=700). PDFiPlusRouter, VaultFactory, InvoiceVault, ClaimToken and InvoiceLogic are verified on Sepolia Etherscan, and we use the canonical Aqua deployment at 0x1111113CCf1426A8E30e2bfF5E005d929bF6a90a. Aqua. Strategies are immutable and Aqua's safeBalances needs both tokens shipped, so a financier ships [USDC: X, CLAIM_1042: 0, CLAIM_1043: 0, ...]. Listing a new invoice means docking the old strategy and shipping a new one; the financier dashboard does both in one flow. Several financiers shipping strategies over the same claims gives the merchant a set of competing bids, and claim holders redeem pro-rata from the vault once the invoice is paid, late fees included. ENSv2 on Sepolia. Each merchant owns <label>.eth with its own resolver. Under it, invoices.<label>.eth has a separate registry and resolver, and each invoice is registered there with its vault as the addr record plus text records for the canonical order hash and the PDF hash. The backend holds a billing key with roles only on the invoices registry and resolver, so it can publish invoices but can't touch the merchant's main name, and the merchant can revoke it in two transactions. We gave invoices their own resolver because ENSv2 resolver roles apply to the whole resolver. A stolen billing key can break an open invoice by clearing its record, but it can't redirect money, because the opcode also checks the maker and the CREATE2 vault. Invoice names never expire, since an expired name would resolve to nothing and block payment. Backend (Express, TypeScript, MongoDB), carried over from PDFi: Coinbase CDP server wallets on ethereum-sepolia and a parser for the FDF forms Acrobat submits. New for Tokyo: accounts with usernames, scrypt password hashes, per-account and per-IP rate limits, signed sessions and wallet sign-in by signature; invoice publishing through the billing key; and a testnet faucet that gives new accounts gas and demo USDC. The backend generates each PDF with pdf-lib and embeds Acrobat JavaScript that calls submitForm with only the action and the data field. Our SDK (@pdfi-plus/sdk, viem) builds and decodes orders, quotes payments and factoring bids, and finds live strategies from Aqua's shipped and docked events in a single getLogs scan. Frontend: Next.js 15 and Tailwind v4 on Vercel, Dynamic for browser wallets. The backend runs in Docker Compose behind Caddy on an ARM VM and redeploys itself when main changes. The hacky part: of the PDF viewers we tested, only Adobe Acrobat desktop runs the form JavaScript, so the in-PDF payment works there and every PDF also carries a pay link and QR for the web pay page. PDF JavaScript is unsigned, so a tampered PDF could change the submit URL. That is why the pay page, which verifies the merchant's EIP-712 signature and shows the ENS-checked payee, is the primary path, and the in-PDF button is the convenience path for people without a wallet.