Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 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