takers

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:

  1. Discovers it by scanning the contract's OfframpProcessed logs and asking the escrow which of those deposits still hold liquidity.
  2. Prices it against the min and max intent size in its config.
  3. Stakes USDC equal to the intent in zk-p2p's StakeVault, because signalIntent reverts without it.
  4. Claims it with signalIntent on OrchestratorV3.
  5. Pays the Venmo from a browser tab the operator already logged into, driven over the Chrome DevTools Protocol.
  6. 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.
  7. 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

itemvaluesource
MarginThe 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 lockFourteen days after release. The vault's unlockStake is controller-only; nobody can pull it early.StakeVault, observed after the 2026-09-01 fill
Venmo floatDollars leave your Venmo before USDC arrives. Fiat first, proof second.Order of operations
Chargeback exposureVenmo is reversible. The fourteen-day lock is zk-p2p's dispute window, not a bug.zk-p2p StakeVault design
size it honestly

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=9222 and 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 --recipient for 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 timestamp bites

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 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.path with 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.