Run a taker
A taker is the other side of every zpay order: the account that pays the Venmo and collects the USDC. The daemon does the watching, claiming, paying and proving. You supply a Base key, a stake, and a browser that is logged into Venmo.
What a taker does
When a zpay user sends ZEC, the swapped USDC lands on the zpay contract, which posts it to zk-p2p's EscrowV2 as a deposit with a rate floor and a fixed payee hash. That deposit is open to anyone. A taker:
- Discovers it by scanning the contract's
OfframpProcessedlogs and asking the escrow which of those deposits still hold liquidity. - Prices it against the min and max intent size in its config.
- Stakes USDC equal to the intent in zk-p2p's StakeVault, because
signalIntentreverts without it. - Claims it with
signalIntenton OrchestratorV3. - Pays the Venmo from a browser tab the operator already logged into, driven over the Chrome DevTools Protocol.
- Proves the payment by sending a Venmo session cookie to zk-p2p's Nitro enclave, which replays the payment feed and signs an EIP-712 attestation.
- Releases the USDC to itself with
fulfillIntent, which the on-chain verifier accepts only with that signature.
Every step before the Venmo send is reversible. The send is not. The daemon's payment sequence has exactly one step that moves money, it is the last one, and a unit test holds that property.
What it pays, and what it costs
| item | value | source |
|---|---|---|
| Margin | The difference between the order's rate floor and one dollar per USDC. Default floor is 1.00, so the margin is whatever the user set below that. | Currency.minConversionRate on the deposit |
| Gas per fill | $0.031 for the signal and release pair, 1.34M gas at 0.0096 gwei. | Measured on Base, 2026-09-01 |
| Stake lock | Fourteen days after release. The vault's unlockStake is controller-only; nobody can pull it early. | StakeVault, observed after the 2026-09-01 fill |
| Venmo float | Dollars leave your Venmo before USDC arrives. Fiat first, proof second. | Order of operations |
| Chargeback exposure | Venmo is reversible. The fourteen-day lock is zk-p2p's dispute window, not a bug. | zk-p2p StakeVault design |
Serving N orders at once needs N times the stake and N times the Venmo balance, and the stake does not recycle for two weeks. Ten $5 fills a day is $700 of committed stake before the first dollar comes back.
What you need
- A Base key in
TAKER_PRIVATE_KEY, holding ETH for gas and USDC for both the stake and any payment it fronts. - Chrome started with
--remote-debugging-port=9222and signed into Venmo. The daemon attaches to that tab. It never sees your password and never logs in. - A config file, copied from the example below, with the zpay contract address filled in.
- A coordinator token, if you want the daemon to look up which handle a deposit pays. The chain only stores a hash. Ask the coordinator operator, or pass
--recipientfor an order you were told about directly.
Install
Rust 1.75 or newer. The proof script needs Node 20 or newer.
# build the daemon git clone https://github.com/zpay-cash/auto-taker cd auto-taker cargo build --release # the prover lives beside it (cd scripts/proof && npm install) # a Chrome you are logged into Venmo with chromium --remote-debugging-port=9222 --user-data-dir=$HOME/.zpay-chrome
Configure
Copy the example and set glue_contract to the zpay contract. Everything else has a working default for Base mainnet.
cp config.taker.example.toml config.taker.toml
# config.taker.toml [network] base_rpc_url = "https://mainnet.base.org" chain_id = 8453 [contracts] usdc = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" zkp2p_escrow = "0x777777779d229cdF3110e9de47943791c26300Ef" # EscrowV2 zkp2p_orchestrator = "0x014025fDE093f8701d86e9f38e2C3a9b779cb5c7" # OrchestratorV3 stake_vault = "0x47c26258222e2f96424bD2B21bf173f0DA5034C7" glue_contract = "0xafc314Ea35Bb05AaDb254F5B4A8e05db8e7739A9" # zpay [taker] coordinator_url = "https://coordinator.example" # https, or loopback # coordinator_token = "..." # from the operator max_intent_amount = "100000000" # 6-decimal USDC. 100 USDC min_intent_amount = "1000000" # 1 USDC poll_interval_seconds = 15 lookback_blocks = 5000 max_payment_cents = 2500 # hard ceiling per Venmo send; refuses, never clamps journal_path = "taker-fills.jsonl" [session] path = "venmo-session.json" # written 0600; treat as a password max_age_hours = 12 # refuse to signal on older material [venmo] cdp_url = "http://127.0.0.1:9222" note = "thanks" timeout_seconds = 120 [zkp2p] api_url = "https://api.zkp2p.xyz"
Three of those lines are guards, and each one exists because of a specific
way money could go wrong. max_payment_cents is what stops a
units-versus-dollars mistake from reaching the send button. max_age_hours
stops the daemon from signalling on a cookie that will fail at the enclave
after the fiat has already left. coordinator_url must be https
because the username you are about to pay travels over it; the daemon still
checks that name against the deposit's own on-chain payee hash before paying.
Dry run first
The daemon's default way to try anything is --dry-run. It scans
the chain, prices each deposit, reads your free stake, resolves who would be
paid, and prints every browser step it would take, including the click that
sends money. It sends no transaction and moves nothing.
zecp2p-taker check-venmo # is there a usable, logged-in Venmo tab? zecp2p-taker status # taker address and free stake zecp2p-taker auto --dry-run # report what would happen; stop before every gate
found claimable deposits count=1
dry run: would signal, then pay $25.00 to @alice deposit_id=4392 amount=25000000 free_stake=0
would: open https://account.venmo.com/pay?recipients=alice
would: wait for input[name='amount'], [data-testid='amount-input']
would: type "25.00" into input[name='amount'], [data-testid='amount-input']
would: type "thanks" into textarea[name='note'], [data-testid='note-input']
would: click [data-testid='send-button'], button[type='submit'] <-- sends the money
Run
zecp2p-taker stake --amount 100 # put 100 USDC into the StakeVault zecp2p-taker auto # watch the contract; asks before each of the two money steps zecp2p-taker auto --once # one scan, then exit zecp2p-taker auto --yes # unattended: signals, pays, attests, releases, no prompts
Without --yes the daemon stops at the two irreversible steps,
the claim and the Venmo send, and waits for a yes at the terminal. Everything
that can fail for free runs before the first prompt. With --yes
there is no human left to catch a bad value; the payment cap, the payee
check, the amount read back out of the browser, and the journal are what
stand between a wrong number and a real payment.
The daemon works one deposit at a time. Stake and Venmo balance are finite, and a half-finished payment needs attention before the next claim. A restart reads the journal first, so it can tell which fills may already have moved money.
The proof step
Releasing the USDC needs a payment proof. This is not a zero-knowledge proof and there is no browser extension. zk-p2p's V3 moved verification into an AWS Nitro enclave: the client encrypts a logged-in Venmo cookie to a key whose attestation document it checks against the AWS Nitro root, posts it, and the enclave replays
GET https://account.venmo.com/api/stories?feedType=me&externalId=<SENDER_ID>
from inside the enclave and returns an ECDSA signature over
PaymentAttestation(intentHash, releaseAmount, dataHash). The
verifier at 0xC6F4…502B
checks that signature during fulfillIntent. A plain HTTPS call,
so the daemon makes it.
The verifier compares the attested snapshot against the intent stored on chain and reverts with UPV: Snapshot timestamp mismatch if they differ. The daemon reads the signal timestamp off the orchestrator for exactly this reason. An attestation built from the wall clock verifies locally and fails on chain.
The signature is bound by EIP-712 to chain 8453 and that verifier address. It cannot be produced for a testnet and cannot be replayed anywhere else.
If you paid and cannot produce a proof, cancel gives back the
claim so the maker's USDC is not stranded and your stake unlocks. Your
dollars have already left at that point; this is the way out, not a refund.
zecp2p-taker fulfill --intent 0x… --proof attestation.json zecp2p-taker cancel --intent 0x…
The cookie is the sharpest edge
The session material the enclave replays is a live Venmo credential. It is encrypted to the enclave's attested key before it leaves your machine, so the service operator cannot read it outside the enclave. It is still equal in power to being logged in as you, and the enclave enforces no capture-age limit and no one-use limit on it.
- The daemon writes it to
session.pathwith mode 0600 and never logs it. - One capture serves many fills. When it goes stale, capture it again from the same browser; the daemon refuses to signal on material older than
max_age_hours. - Venmo ties a session to the browser's User-Agent closely enough that a mismatch can fail the replay. Capture and replay from the same browser.
- Run the daemon on a machine you would be comfortable leaving logged into Venmo.
Racing and failure
Two takers can signal on the same deposit and one will revert. That is an open market working as designed. The daemon logs the loss and moves to the next deposit. If the Venmo payment itself fails, the daemon cancels the intent so the stake unlocks and the maker's order reopens.
An intent nobody proves expires after six hours and the escrow's
pruneExpiredIntents releases the deposit back to the order.
A taker who signals and walks away gets their stake back when the intent
expires; the deadline has no teeth beyond the wait.
Filling your own orders
The daemon does not care whose order it fills. Run it under the same Venmo account you named as payee and it will claim your own deposit, pay yourself, prove it and release the USDC back to your key. Your ZEC becomes dollars in your Venmo with nobody else in the loop, and the stake it locks is your own money against your own order.
zecp2p-taker auto --yes --only-user 0xYourBaseAddress
--only-user restricts the daemon to sessions that address
opened. Leave it off and the daemon serves every zpay order it sees, which
means fronting fiat for strangers.
This is the shape of the first mainnet fill, on 2026-09-01: a $4.84 Venmo payment released 4,875,437 units of escrowed USDC against an enclave attestation. Every step of that one was driven by hand. The daemon is the transcription.