Inspiration

PayLink is built in Antananarivo for freelancers who are paid from abroad. Their clients want to pay the way they already do: many with PayPal, some with dollars on chain. Offering both today means two invoices for one job, two places to watch, and a real chance of being paid twice, or of chasing a client who already paid. Madagascar is where this started, and where it is sharpest: secondary sources say Malagasy PayPal accounts may be able to send but not receive (we could not verify it), so a freelancer there cannot count on the PayPal rail alone, and the same invoice's on-chain rail is the one that still gets them paid. The product is for freelancers anywhere PayPal lets them receive, with Madagascar as the case that shows why one invoice should carry both rails. Our working hypothesis, which we are testing in structured interviews and claim no results for, is that the chasing and the checking are the hard part. PayLink Twin gives one signed invoice two rails, PayPal and dollars on chain (USDC, or PayPal USD), and gives the follow-up to a desk that can propose but cannot touch the money.

What it does

  1. Draft. The freelancer pastes the client's message ("Love it! Send me the invoice, $120, I'll pay by Friday"). The assistant extracts what the job is, the client and the due-date phrase; code parses the amount and the date from the message itself, so a number the model invents never becomes the invoice amount. Every value lands in an editable form.
  2. Sign. One EIP-712 signature in the freelancer's wallet makes the PayLink invoice: a link and a QR card, no transaction, no gas.
  3. Twin it on PayPal. One tap creates a PayPal invoice for the same amount, with the link's key in PayPal's own reference field, shares it, and reads it back from PayPal before saying it exists.
  4. One link, two rails. The client opens the link. Four checks are read from the chain first (signature, network, genuine contract, still payable). Then "Pay with PayPal" goes to PayPal's hosted invoice page, shown only when PayPal's amount equals the signed amount and the link is PayPal's own payer page, at an address pinned in the code; or the client pays in USDC (or PayPal USD) on Ethereum Sepolia from their own wallet, with one EIP-3009 authorisation.
  5. Paid means checked. A PayPal webhook is verified with PayPal and then only triggers a read of the invoice from PayPal; a chain payment counts only as a verified Paid event at the exact amount. The freelancer's till turns green "via PAYPAL · verified with PayPal" or for the chain payment, never because of a message.
  6. The collections desk. For every invoice, a deterministic table of ten situations reads both rails and decides what may happen next, with exact values. Paid on PayPal: cancel the PayLink link so the client cannot pay twice (the freelancer taps, then signs the cancellation in their own wallet). Paid on chain: the payment is recorded on the PayPal invoice automatically, with an Undo for 24 hours, so both books agree. Overdue: PayPal's own reminder when PayPal has the client's email (PayPal refuses one without it, and the desk says why), or a reminder message in the client's language for the freelancer to send. Cancelled on one rail: cancel on the other. Paid twice: an alert with the refund steps, never automatic. "Check my invoices" reads every open invoice again on PayPal and on the chain.
  7. Was I really paid? Paste a payment notice; the verdict (received, partly, not received, can't check now) comes from PayPal and the chain only, never from the notice.
  8. Status. One page shows the API, PayPal, the last verified webhook, the assistant's neurons left today, the store and the contract.

The app is in English, French and Malagasy, and is tested at 320 px wide in light and dark themes with axe-core.

How we built it

PayPal. PayPal REST in the sandbox, from a Cloudflare Worker: OAuth 2.0 client credentials (the token cached in a Durable Object); Invoicing v2 search (by our reference, so a retried twin is found instead of duplicated), create, send, get (the truth for every PayPal state), record payment, delete payment (Undo), cancel and remind; Webhooks verify-webhook-signature (the delivery's raw bytes posted back to PayPal) and webhook creation for INVOICING.INVOICE.PAID, CANCELLED and UPDATED. Every write carries a PayPal-Request-Id; a write whose answer is lost is settled by reading, never repeated blind. The client has no operation that captures, refunds, pays out or disputes money. The desk's read tools follow the PayPal Agent Toolkit tool contract (names and schemas parity-tested against @paypal/agent-toolkit 1.11.0) and read only the freelancer's own reconciled invoices. Because PayPal's sandbox API could not be reached from our development environment, we built against PayPal's OpenAPI files and a PayPal fake whose every answer is validated against those schemas, then ran a capture script against the real sandbox (2026-10-29): it recorded how PayPal answers (a shared invoice reads UNPAID, a recorded payment MARKED_AS_PAID, the payer page's address, a reminder refused without the client's email, a reference search that also matches partially), and the code and the fake now follow it. Real sandbox payments proved the webhook path: each delivery verified with PayPal, then processed. What the run could not observe stays marked UNVERIFIED in the repository.

AI. Cloudflare Workers AI through the Worker's binding: @cf/google/gemma-4-26b-a4b-it, with @cf/meta/llama-3.3-70b-instruct-fp8-fast as fallback. Two jobs. Drafting: one structured call; personal data is replaced by placeholders before the model sees the message, and code parses amounts and dates. The desk: six read tools and propose_action, which only queues a proposal if the deterministic fence allows that action, and keeps the fence's values, never the model's; a reminder's text is kept only if its amount, its dates and its invoice number are that invoice's own, as code reads them from the record. Seven invariants are each enforced by a named test: the model cannot sign or submit a transaction, cannot move PayPal money, every effect needs a tap on exact values, no model text on money screens, numbers come from code, payment truth is PayPal's GET or the chain, untrusted text is data. Memos, PayPal notes and client names never reach the desk's model. An injection suite of 40 cases blocks a release, and an end-to-end test drives a model that obeys every injected instruction through the real provider, desk and fence: only the fence's own proposals survive. The assistant runs on the free daily allocation; when it is spent, it rests and every manual path works.

Chain and app. PayLinkV2 (Solidity 0.8.30, OpenZeppelin 5.3.0): immutable, ownerless, fee-less, exact-delta settlement, deployed by CREATE2 at the same address on every chain, its source verified on Etherscan. API: Hono and zod on Cloudflare Workers, SQLite-backed Durable Objects (one per payee, one global), one cron a minute. Web: strict TypeScript without a UI framework, a PWA with its own Content-Security-Policy and Trusted Types, viem.

Tests. 335 Foundry tests for the contract; more than 2,100 TypeScript unit and integration tests, 487 of them for the API; 51 Playwright end-to-end tests for this edition: every screen state in English, French and Malagasy, light and dark, with axe-core, and both rails end to end on a fork of the real Ethereum Sepolia with Circle's USDC contract and the PayPal fake.

Challenges we ran into

  • No route to PayPal's sandbox from our development environment: we built on a fake validated against PayPal's OpenAPI files, then one capture run on the real sandbox pinned what it does. PayPal's webhook simulator events do not verify by postback, so the webhook path was proven with real sandbox payments.
  • Two rails echo each other: PayLink's own record on the PayPal invoice must never look like a second payment. An echo filter matches PayPal's payment method, our note naming the chain transaction, and a verified Paid event in that transaction.
  • Ethereum Sepolia now runs Amsterdam, whose state-creation gas makes our contract's deployment cost about 19.2 million gas, seven times what our registry assumed. We caught it on a fork of the real chain and re-measured every gas bound before the real deployment.
  • Prompt injection everywhere: client messages, memos and PayPal notes are untrusted text, and the agent's tools can only propose.

Accomplishments that we're proud of

  • An agent that reads both rails and proposes useful next steps, yet structurally cannot move money: the fence is code, approvals are bound to exact values, and the chain cancellation is the freelancer's own signature.
  • Both rails end to end, repeated eight times each without a failure on a fork of the real Ethereum Sepolia: paid on PayPal, then the verified webhook, the desk's proposal and the freelancer's own cancellation on chain; paid in USDC, then the automatic record on the PayPal invoice and its Undo; and injected instructions in a memo and in a PayPal note that change nothing, even with a model built to obey them.

What we learned

PayPal's GET of an invoice is a better source of truth than any event; webhooks are hints. The sandbox shows what no schema says: a payment recorded by the merchant reads MARKED_AS_PAID, a reminder needs the client's email, a reference search also matches part of a reference. An agent becomes trustworthy when its tools are narrower than its prompt. Measured numbers beat assumed ones: the gas surprise came from a fork, not from documentation.

What's next

Publish a live, dated eval scoreboard; per-freelancer PayPal onboarding instead of one demo merchant; PayPal USD as soon as test tokens can be obtained; mainnet only after a legal review of payments and foreign exchange in Madagascar.

Honest status

PayPal sandbox and Ethereum Sepolia testnet only; no real money. One sandbox PayPal merchant stands for every freelancer in the demo; whether a Malagasy PayPal account can receive is unverified, and the product claims nothing about it. Not audited: our own review only. No users, revenue or partners yet. PayLinkV2 on Ethereum Sepolia: deployed on 2026-10-29, transaction 0x9a5ade1164108da82f79a2f522d8247e1822f8f182d87d338484c358fab0e5b3, source verified on Etherscan. Built during the hackathon, from 2026-10-21, on top of PayLink v2 (built from 2026-10-05, also entered in other events); the commit range is in the repository.

AI in the product: Cloudflare Workers AI (@cf/google/gemma-4-26b-a4b-it, fallback @cf/meta/llama-3.3-70b-instruct-fp8-fast). Development assisted by Claude Code (Anthropic); the owner directed, reviewed and is responsible for the work.

Built With

  • claude-code
  • cloudflare-durable-objects
  • cloudflare-pages
  • cloudflare-workers
  • cloudflare-workers-ai
  • ethereum-sepolia
  • fast-check
  • foundry
  • gemma
  • hono
  • llama
  • paypal
  • paypal-sandbox
  • paypal-webhooks
  • playwright
  • pyusd
  • solidity
  • typescript
  • usdc
  • viem
  • vitest
  • zod
Share this project:

Updates

Submission history