尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Pay up to 150 people in one transaction. Each person claims their dollars with a link and a passkey.
Live: demo.fanout.tech (platforms) and wallet.fanout.tech (payees). Runs on Monad testnet with Agora AUSD.
Paying many people across borders is slow and expensive. Bank wires take days, and every hop takes a fee. Small payouts lose the most.
With Fanout, a platform uploads a CSV and one transaction pays everyone in AUSD, a US dollar stablecoin. Each payee gets a claim link by email and signs in with a passkey. They never buy gas, write down a seed phrase or install a crypto wallet.
The landing page compares a $500 payout: about $448.50 arrives after a week by wire, and $499.90 arrives with Fanout. These are illustrative numbers, defined in fees.ts.
It takes about 3 minutes. You need two email inboxes you can open (or one with plus addresses, like [email protected]).
email and amount columns (up to 150 rows), review it, and send. One transaction pays everyone.Passkeys: save the passkey to iCloud Keychain (Safari, or Chrome on a Mac with iCloud Keychain) or to Google Password Manager. Desktop Chrome's local profile store can lose the passkey, and with it the way into that account.
For platforms
csv.ts, new-payout-flow.tsx, BatchPayout.solclaim-email.ts, claim-link-actions.tsxclaim-counter.tsx, batch-detail.tsxclaim-reminders.tsx, batch-export.tsClaimEscrow.solFor payees
claim-flow.tsx, passkey-account.ts, relayer.tsfx.tsSettleToUsdc.sol, usdc-offer.tsxerc3009.ts, send-flow.tsx, email-send-flow.tsxmanifest.ts, install-prompt.tsx, flowchart LR
P[Platform] -->|deposit AUSD| T[Treasury]
P -->|"createBatch(rows)<br/>one transaction"| B[BatchPayout]
B -->|debit total| T
T -->|AUSD| E[ClaimEscrow]
B -->|open one claim per row| E
E -->|claim via relayer, no gas| W[Payee passkey account]
E -.->|refund after 30 days| T
W -->|optional| S[SettleToUsdc] -->|AUSD to USDC| A[Agora stable-swap pair]
W -->|gasless send, ERC-3009| R[Anyone]
T & B & E -.->|events| I[Envio indexer] -.->|GraphQL history| UI[Dashboard and wallet]
BatchPayout.createBatch takes the whole CSV. In one transaction it debits the platform's balance, moves the total to ClaimEscrow, and opens one claim per row.Contract details: smart-contract/README.md.
Monad testnet, chain id 10143. These are the contracts the live site uses (the monad-v3 deployment).
| Contract | Address |
|---|---|
| Treasury | 0xecC2616C45a55AEA2d33374E4B99D64d0900255c |
| BatchPayout | 0x01aD7B7A7Ab17ffE4fDFE4644828167702338386 |
| ClaimEscrow | 0x15DaAD3E6200051AE2F956ba32cD8033e82d40B6 |
| SettleToUsdc | 0xA1ac3cBe75697e4Ad7C5fF393EbC3AE9fa67DeC2 |
| AUSD (Agora) | 0xa9012a055bd4e0eDfF8Ce09f960291C09D5322dC |
| Agora AUSD/USDC pair | 0x1Aa8958Aa34cEC8096EF4381cb335effe977b0ae |
Example transactions:
| What | Transaction |
|---|---|
| Deploy Treasury | 0x0664…40b1 |
| Deploy ClaimEscrow | 0x1892…5c20 |
| Deploy BatchPayout | 0x9ee3…357b |
| Deploy SettleToUsdc | 0xd77a…a86e |
| Platform deposits 1.51 AUSD for batch 1004 | 0x6bf0…0c6b |
createBatchFor: 150 people, one transaction, 11.17M gas (batch 1004) | 0x35c7…8195 |
| First claim from batch 1004, sent by the relayer | 0xc975…9d29 |
| Last (150th) claim from batch 1004 | 0x7807…e4a3 |
Platform deposits 1,000 AUSD (previous monad-ausd contracts) | 0xb6a4…890e |
| : 5 people, $137, one transaction () |
Batch 1004 is a full-size payout on the live contracts: the platform signed it and the relayer submitted it. All 150 payees then claimed, each to a new wallet, in blocks 70,727,866 to 70,728,109. The indexer shows it as 150 rows, 150 claimed.
Deploy records, including every transaction: smart-contract/ignition/deployments/.
AI agents can run payouts through Fanout, with a person in control. There are two ways in.
1. MCP, for an agent working for a platform. On the dashboard's Agents page a platform creates an agent key and sets its limits: most per payout, most per day, most people per payout, an optional allowlist of emails or domains, and when the key expires. The platform signs these limits once with its own account (an EIP-712 AgentPolicy, checked on the server; no contract changes). The key is shown once; Fanout stores only its SHA-256.
The agent connects to /api/mcp (Model Context Protocol, Streamable HTTP, TypeScript SDK v2) with Authorization: Bearer fo_agent_.... Its tools:
| Tool | What it does |
|---|---|
create_payout | Asks to pay up to 150 people by email ({ email, amount: "25.00", note? }, the same rules as a CSV upload). Returns approval_needed with an approval link, or the sent payout if the platform approves while it waits. Takes an idempotency_key. |
get_payout_status | Where a payout stands, and each person's status: claimed, waiting or returned. |
list_payouts | This agent's payouts, newest first. |
send_reminders | Emails people who haven't claimed yet, at most once a day. |
return_unclaimed | After the claim window, puts unclaimed money back in the platform's balance. |
get_balance, get_policy | The payout balance, and the agent's limits and how much of today's is left. |
Amounts are always strings in dollars with two decimals, never floats. Errors say what to fix, row by row.
Every payout needs the platform's approval. The agent never signs anything that moves money. create_payout builds the payout on the server (claim keys included, kept encrypted with AGENT_SECRET so the links can be emailed and reminded) and files an approval request. Within the limits it's a one-tap approval (and a push notification, if the platform turned them on); over them, the approval page lists exactly which limits it goes over and asks for a full review. The platform's browser checks the payout against the rows it shows, keeps the claim links, and signs the CreateBatch with the platform's account; the relayer then submits createBatchFor, which checks that signature onchain. A paused key (the kill switch), a revoked key or an expired one can't ask at all, and pausing cancels what's waiting.
Why not let payouts within the limits go through on their own? CreateBatch commits to each row's claim signer and amount, so signatures made in advance (like the CRE schedule) only work for payouts known in advance. Anything else would need Fanout to hold a key that can spend the platform's balance, which this design avoids. A platform account that is a smart account could later grant a bounded session key instead (createBatchFor already accepts ERC-1271 signatures).
Connect it from the Agents page (Claude Code, Cursor, Claude Desktop through mcp-remote, or an OpenAI Responses API mcp tool), or with Claude Code:
claude mcp add --transport http fanout https://demo.fanout.tech/api/mcp --header "Authorization: Bearer fo_agent_..."
Try it locally: pnpm dev, then pnpm --filter web mcp-smoke --url http://localhost:3000 creates a key for a demo platform, connects an MCP client, asks for payouts within and over the limits, approves one as the platform and checks it.
2. x402, for an agent with no Fanout account. POST /api/x402/payout with rows and an Idempotency-Key answers 402 Payment Required with x402 v2 requirements in the PAYMENT-REQUIRED header (HTTP transport): scheme exact, the payout total plus a fee (0.5%, at least $0.10), USDC on Monad testnet, paid to Fanout's operator account. The agent signs an ERC-3009 authorization for exactly that and retries with PAYMENT-SIGNATURE. Fanout verifies it, checks it can make the payout and email it, settles the payment, pays the rows from the operator account's payout balance, emails the claim links, and answers with the payout number, a status URL (GET /api/x402/payout/<id>, counts only, no emails) and PAYMENT-RESPONSE.
curl -i -X POST https://demo.fanout.tech/api/x402/payout \
-H "content-type: application/json" -H "Idempotency-Key: payout-0001" \
-d '{"rows":[{"email":"[email protected]","amount":"25.00","note":"Thanks!"}]}'
Limits: 50 people and $1,000 per payout by default. The same Idempotency-Key with the same body returns the first answer and is never charged twice; with another body it's refused. If the payout fails after the payment settled, the operator sends the payment back to the payer at once; if even that fails, the key's record keeps the payer and transaction (refund_due) for a manual refund. Unclaimed money returns to the operator's balance after the claim window; returning it to the payer is manual for now.
What's verified and what isn't (checked 2026-10-08):
https://x402-facilitator.molandak.org, for testnet (eip155:10143) and mainnet (eip155:143), with Circle USDC (testnet 0x534b…43A3, mainnet 0x7547…b603, EIP-712 domain USDC / 2) (guide, agentic payments). Its /supported endpoint answered with exact (x402 v2) on both networks. Coinbase's CDP facilitator doesn't list Monad (network support).X402_FACILITATOR=self: Fanout checks the ERC-3009 signature, balance and nonce itself and its relayer submits transferWithAuthorization.X402_NETWORK=monad) needs the payout contracts on mainnet too; the endpoint refuses a network that isn't the contracts' chain.chains.ts, hardhat.config.tsSettleToUsdc.sol, usdc-settle.ts, erc3009.tspasskey-account.ts, passport-key.tslib/auth/Prerequisites: Node 22 or later, pnpm 9 (corepack enable). Foundry is optional, for cast. Docker is needed only to run the indexer locally.
pnpm install
cp apps/web/.env.example apps/web/.env.local
pnpm dev # http://localhost:3000
Mock mode (default, no keys needed). With NEXT_PUBLIC_USE_MOCK=true the app uses an in-memory client and a local sign-in. No chain calls. On a payout page, Simulate people claiming fills the claim counter.
Onchain mode. Set NEXT_PUBLIC_USE_MOCK=false and fill in apps/web/.env.example:
NEXT_PUBLIC_PRIVY_APP_ID, PRIVY_APP_SECRET: email sign-inRELAYER_PRIVATE_KEY: a testnet-only key funded with MON (faucet); pays gas for claims, sends and USDC swapsVERIFIER_PRIVATE_KEY: co-signs claims; its address must be ClaimEscrow.verifierRESEND_API_KEY, CLAIM_EMAIL_FROM: claim emailsNEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, NEXT_PUBLIC_POSTHOG_HOST, AURORA_APP_KEY, NEXT_PUBLIC_INDEXER_URLContract addresses default to the deployment in contracts.generated.ts.
To make a test CSV (only use inboxes you control; every row gets an email):
pnpm --filter web demo-csv --to [email protected] # 150 plus addresses of your own inbox
Checks
pnpm --filter web test # unit tests (Vitest)
pnpm lint
pnpm typecheck
Contracts (smart-contract/, Hardhat 3):
pnpm --filter smart-contract build
pnpm --filter smart-contract test
pnpm --filter smart-contract typecheck
pnpm --filter smart-contract export-abis # copy ABIs and addresses into the web app
Deploying needs MONAD_PRIVATE_KEY in smart-contract/.env (see smart-contract/.env.example). The deploy scripts are in smart-contract/package.json.
Indexer (indexer/, its own pnpm project, not part of the root workspace):
cd indexer
pnpm install
cp .env.example .env # ENVIO_API_TOKEN
pnpm codegen
pnpm test # handler tests, no network needed
pnpm dev # local indexer, needs Docker
Recurring work (cre/, Chainlink CRE workflows in TypeScript, run with Bun):
refund-expired: on a cron, asks the indexer for payments still unclaimed after their claim window and sends one report that refunds them (ClaimEscrow.refundMany) to the platforms that paid them.scheduled-payouts: on a cron, reads a schedule of payouts the platform signed ahead of time (one CreateBatch authorization per period, made with cre/scripts/sign-schedule.ts), checks each due one onchain, and sends it (BatchPayout.createBatchFor).Both send their reports to FanoutKeeper, which accepts them only from the Chainlink forwarder and, once configured, only from the expected workflow owner and workflow ids. Neither workflow can move money anywhere but where the platform already agreed: refunds always go back to the paying platform, and every scheduled row is covered by the platform's signature.
pnpm cre:setup # install the workflows' dependencies
pnpm cre:test # workflow tests with the CRE SDK's test runtime
pnpm cre:simulate # end to end on a local fork of Monad testnet (needs anvil)
cre:simulate forks Monad testnet, deploys FanoutKeeper on the fork, creates an expired and an open payout and two scheduled ones, then runs cre workflow simulate --target local-simulation --broadcast for both workflows and checks the result onchain: expired rows refunded, the open row untouched, the due scheduled payout created. cre workflow simulate needs a CRE account (cre login); without one the script delivers the same reports through the fork's MockKeystoneForwarder instead and runs the same checks.
To deploy: deploy FanoutKeeper with ignition/modules/FanoutKeeper.ts and the forwarder for the chain (forwarder directory), put its address in config.production.json for both workflows, then cre workflow deploy <workflow> --target production-settings (deploying needs CRE deploy access). Finally call setExpectedWorkflowOwner and setWorkflowIdAllowed on the keeper.
apps/web/ Next.js app: landing page, platform dashboard, claim page, payee wallet, /verify
app/api/ relayer (claim, send, settle), claim emails, FX rates, Aurora
lib/fanout/ contract client (onchain and mock), relayer, claim keys, indexer queries
lib/payee/ passkey account, app lock, Earnings Passport
smart-contract/ Treasury, BatchPayout, ClaimEscrow, SettleToUsdc, FanoutKeeper; tests; Ignition deploy records
indexer/ Envio indexer for the contract events
cre/ Chainlink CRE workflows: expired-payment refunds, scheduled payouts; local fork simulation
Fanout — PRD.md product requirements
aurora.ts, cross-chain-dialog.tsx/verify page. passport.ts, passport-key.ts, verify-view.tsxcreateBatchmonad-ausd0x74cb…9e16 |
A payee's claim from that batch, sent by the relayer (monad-ausd) | 0xd11d…be82 |
indexer/, indexer.tsaurora.ts, aurora-server.tslib/ai/, lib/assist/, app/api/ai/instrumentation-client.ts, scrub.ts