A payment plugin for Pretix that accepts crypto payments. Two modes: direct WalletConnect checkout (user pays gas) and x402 gasless flow (relayer pays gas for USDC/USDT0). All payments verified directly on-chain — no vendor dependency.
- Chains: Ethereum, Optimism, Polygon, Base, Arbitrum
- Tokens: USDC (all chains), USDT0 (Optimism, Arbitrum), native ETH (all chains)
User connects wallet, picks token+network, sends a standard ERC-20 transfer or native ETH send, plugin verifies on-chain.
- Buyer selects "Crypto" at checkout and confirms the order
- Pretix creates the order and redirects to the payment page
- Buyer connects wallet via WalletConnect (MetaMask, Rainbow, Coinbase Wallet, etc.)
- The picker fetches the wallet's per-(chain, token) balances via
/plugin/wc/wallet-balances/(Zapper-first, RPC fallback — same engine the x402 flow uses) and displays them inline. Rows where the balance is below the order amount are tinted to flag clearly-empty wallets; the heuristic uses the live ETH price piggy-backed onpayment-optionsso the picker check matches what the server enforces at quote time. - Buyer picks token and network, clicks "Pay now"
- Plugin creates a quote (locked price, 10-min expiry) with a SIWE-lite signature challenge
- Buyer signs the challenge (proves wallet ownership) then confirms the on-chain transfer
- Plugin verifies the transaction on-chain via RPC
- Order is marked as paid
Buyer signs an EIP-3009 transferWithAuthorization; a relayer broadcasts it and pays gas. No ETH needed in the buyer's wallet for stablecoin payments.
- Frontend creates a purchase order via
/plugin/x402/purchase/(pricing includes variations, addons, voucher discounts, crypto discount) - Frontend POSTs to
/plugin/x402/payment-options/with the buyer's wallet — plugin returns a list ofPaymentOption[](per chain + token) with balances, sufficiency flags, and a pre-builtsigningRequest(EIP-712 typed data for USDC/USDT0, oreth_sendTransactionparams for ETH) - Buyer picks an option and signs in their wallet (no gas for stablecoins)
- For gasless stablecoins: frontend submits
{authorization, signature}or{authorization, rawSignature}to/plugin/x402/relayer/execute-transfer/; plugin validates the authorization terms (recipient, amount, expiry) and broadcasts via relayer - Frontend polls
/plugin/x402/verify/until on-chain confirmation - Plugin creates the Pretix order (with full variation / addon / answer / voucher parity) and marks it paid
Same as WalletConnect flow but with an additional ethPayerSignature at verify time that cryptographically binds the payer's wallet to the order. Supports:
- EOA (ECDSA recovery)
- Smart wallets (ERC-1271, with ERC-6492 unwrapping for counterfactual wallets)
- EIP-7702-delegated EOAs (e.g. MetaMask Smart Account in 7702 mode) — the verifier detects the
0xef0100…code prefix and retries ERC-1271 against the wallet's chain-bound EIP-712 envelope (viaDOMAIN_SEPARATOR()) when the plain EIP-191 hash is rejected - ERC-4337 bundler flows —
debug_traceTransactionwalks internal calls to locate the actual ETH transfer when the outertx.fromis a bundler EOA
Slippage tolerance: ETH verification accepts up to 0.5% under-payment vs. the quote (industry default, same as Uniswap). This absorbs two real-world drift sources without the merchant needing to over-quote:
- ETH spot price moves between when our oracles fetch it and when the wallet signs
- Smart-account wallets (notably MetaMask 7702 mode) re-derive
valueat signing time using their own price feed instead of passing the exact wei amount through
USDC/USDT0 transfers stay strict — stables don't drift, and the EIP-3009 typed-data signature commits to an exact value.
- Stablecoins: 1 USDC = 1 USD (direct mapping)
- ETH: 4 oracles — Coinbase + Binance.US + Kraken + Bitstamp. Quorum logic: largest cluster of ≥2 prices agreeing within 5% wins; rest are dropped. Tolerates one or two oracles being unreachable.
- POL: 3 oracles — Coinbase + Binance.US + CoinGecko, same quorum.
- Cache: Successful quotes cached for 30s (Django cache backend). Failures aren't cached, so a transient outage retries immediately.
- Vouchers: Supported — set/subtract/percent price modes, per-item targeting.
- Crypto discount: Configurable percentage off, stacks with vouchers. Surfaces on the Pretix order as a negative
OrderFee(fee_type='payment')row for both the WC-native and x402 paths. - Addon
price_included: Honored on the x402 path — addons whose parent ticket'sItemAddOn.price_included=Trueare charged $0 regardless of standalone price.
- Authentication: all
/plugin/x402/*endpoints require a valid Pretix API token (Authorization: Token <token>) — same token system Pretix uses for its own REST API. No custom secrets to manage. - USDC/USDT0 gasless: Payer cryptographically bound via EIP-3009 signature (on-chain verified by token contract). Accepts both EOA (
{v, r, s}object) and smart wallet (rawSignaturehex) formats. - Native ETH: Payer bound via
ethPayerSignature— supports EOA (ECDSA), smart wallets (ERC-1271), counterfactual wallets (ERC-6492), and EIP-7702-delegated EOAs (chain-boundDOMAIN_SEPARATOR()retry). 0.5% slippage tolerance on the on-chainvalueto absorb price drift + wallet-side re-quoting (see Payment flows above) - Smart wallet ETH (ERC-4337):
debug_traceTransactionfallback walks internal call tree to find the actual ETH transfer from the smart wallet - WalletConnect direct: SIWE-lite challenge at quote creation proves wallet ownership
- Relayer binding: Before sponsoring gas, the plugin verifies
authorization.to == configured recipient,authorization.value >= expected amount,authorization.from == intendedPayer, andvalidBefore > now— an attacker with a valid token cannot redirect funds or underpay - Transaction hash is single-use (prevents cross-order replay)
- Chain, token contract, sender, recipient, and amount all verified on-chain
- Rate limiting on purchase and verify endpoints — verify caps at 120/5min per
paymentReferenceand 60/min per IP (sized to fit ~4 minutes of FE 2s polling per payment without false positives) - Atomic claim + reserve prevents double-spend race conditions
- Tx hash dedup is case-insensitive at read (
tx_hash__iexact) and lowercased at write — a mixed-case retry of an already-paid hash is rejected, and the unique-constraint race window between concurrent verifies can't be defeated by case twiddling - Admin manual verify (
/plugin/x402/admin/verify/) intentionally bypasses the off-chainethPayerSignaturecheck for stuck-payment recovery — payer-binding falls back to the on-chaintx.from == intended_payerenforcement insideverify_native_eth. The endpoint is auth-gated by the Pretix API token and intended for operator-only use; the bypass is logged at WARNING for audit. Buyer-facing/plugin/x402/verify/keeps the signature requirement.
pip install -e 'git+https://github.com/efdevcon/pretix-eth-payment-plugin.git@main#egg=pretix-eth-payment-plugin'
python -m pretix migrate pretix_ethAll settings are configurable via the Pretix admin UI (Settings > Payment). No environment variables required — env vars are optional overrides for production hardening.
| Setting | Required | Description |
|---|---|---|
| Receive address | Yes | EIP-55 wallet for the direct-send WalletConnect flow |
| Payment recipient | Yes | EIP-55 wallet for x402 gasless payments (usually same as Receive address) |
| WalletConnect project ID | Yes | Free from cloud.reown.com |
| Alchemy API key | No | Improves RPC reliability; falls back to public RPCs |
| Zapper API key | No | When set, balance lookups (used by both the wc_inject picker and the x402 payment-options endpoint) go through Zapper's GraphQL API in a single round-trip (~200 ms) instead of fanning out RPC eth_calls per chain (~2 s). Falls back to RPC automatically if Zapper fails. Get a key at zapper.xyz/api. |
| Relayer private key | No | Required for gasless USDC/USDT0 (fund the wallet with ETH on each supported chain for gas) |
| Crypto discount % | No | Percentage off fiat price for crypto payments (stacks on top of vouchers) |
| Chain toggles (×5) | No | Enable/disable individual chains (Ethereum, Optimism, Polygon, Base, Arbitrum) |
| Token toggles (×3) | No | Enable/disable individual tokens (USDC, USDT0, ETH) |
| Quote TTL | No | Default 600s (10 min) |
| Min confirmations | No | Default 1 |
| Variable | Purpose |
|---|---|
WC_ALCHEMY_API_KEY |
Overrides Alchemy key (preferred for production — not in DB) |
WC_RELAYER_PRIVATE_KEY |
Overrides relayer key (preferred for production — not in DB) |
WC_VERIFY_RATE_LIMIT_PER_MIN |
Verify endpoint rate limit (default 10) |
For the x402 gasless flow, devcon-next API routes proxy to the plugin using the existing Pretix API token (PRETIX_API_TOKEN_DEV / PRETIX_API_TOKEN_PROD). The plugin validates the token against Pretix's TeamAPIToken table via the Authorization: Token <token> header. No additional secrets needed.
Plugin endpoints (all accept JSON body with organizer + event slugs and camelCase field names):
POST /plugin/x402/purchase/— create pending order (returns HTTP 402 +{paymentDetails, orderSummary}); supports tickets, variations, addons, answers, voucherPOST /plugin/x402/payment-options/— given{paymentReference, walletAddress}, returnsPaymentOption[]with balance, sufficiency, and pre-builtsigningRequest(EIP-712 for USDC/USDT0 oreth_sendTransactionfor ETH)POST /plugin/x402/relayer/prepare-authorization/— returns EIP-712 typed data for a specific chain/token choice (alternative to payment-options for clients that don't want balances)POST /plugin/x402/relayer/execute-transfer/— relayer broadcasts the signedtransferWithAuthorization; validates authorization terms against the pending order before spending gasPOST /plugin/x402/verify/— verifies on-chain tx, creates Pretix order +OrderPositionrows (variations/addons/answers/voucher), confirms paymentGET /plugin/x402/admin/orders/— list completed + pending ordersGET /plugin/x402/admin/stats/— dashboard aggregates (counts, total_usd via DB aggregate)POST /plugin/x402/admin/refund/?action=initiate|confirm|fail— refund state machine
Frontend field names: camelCase (paymentReference, chainId, txHash, walletAddress). The plugin's request body parser also accepts snake_case (payment_reference, chain_id, etc.) for non-frontend clients.
These are documented, non-blocking items for a future iteration:
- Event-level authorization check:
require_pretix_tokenvalidates that the token is valid and active, but does not yet check that the token's team has access to the specific(organizer, event)being operated on. Acheck_team_event_accesshelper exists inx402/auth.pyready to wire in. Marked asTODOinviews_x402.pyandviews_admin.py. - Agent endpoint (
/purchase/[email].tsin devcon-next): currently stubbed at HTTP 501. If x402 SDK agents need to work, add a/plugin/x402/purchase-agent/endpoint that skips theintendedPayerrequirement. - Verify cooldown: the 10-second-between-attempts cooldown from devcon's ticketStore was removed during Phase 3 (conflicted with a test that didn't mock time). The 10/hour and 30/minute limits still apply — add the cooldown back with time-mocked tests if spam protection needs tightening.
- Direct browser → plugin calls: the public endpoints (
purchase,payment-options,relayer/*,verify) currently require a server-side Pretix API token. If we want to skip the devcon-next proxy entirely and have the browser call the plugin directly, we'd need to drop theAuthorization: Tokenrequirement on those specific endpoints and add CORS. - Admin-initiated refunds for legacy rows: the on-chain refund button in the devcon admin UI is gated to
source === 'x402'because the refund CAS state machine (refund_status/refund_tx_hash/refund_meta, locked viaSELECT FOR UPDATEonpayment_reference) only exists onX402CompletedOrder.WCPaymentAttemptandSignedMessagehave no refund columns, so double-refund protection is missing. Fix: introduce a unifiedManualCryptoRefundmodel keyed(source, row_id)with a unique constraint; refund UI branches request path bysourcewith no schema change to legacy tables. - Relayer balance monitoring (from the gas-condition rework):
assert_gas_conditionsno longer blocks a tx when the relayer wallet is low; a drained relayer now surfaces only as a customer-facing 502. Add an admin-UI balance dashboard or periodic probe so operators see low balances out-of-band. Marked as a TODO inx402/gas.py. - Native Pretix-checkout UI upgrade to x402 parity: the
WalletConnectPaymentprovider (payment.py+checkout_payment_confirm.html+static/wc_inject/dist/bundle.js) still uses the legacy/plugin/wc/*direct-send flow (user pays gas, writes toWCPaymentAttempt). Port it to the x402 stack so Pretix-native checkout gets balance fetch, multi-chain/token picker, gasless EIP-3009, and writes toX402CompletedOrder. Sketch: add a native-purchase endpoint that takes an existingorder_code(instead of creating one on verify), a same-origin auth bridge so the browser can call/plugin/x402/*without a TeamAPIToken, and port the pay-column logic fromdevcon/src/pages/tickets/store/checkout.tsx. Estimated ~1 to 1.5 weeks (~3 days for a stripped single-chain variant). - Per-item crypto-payment disable: allow admins to mark specific items (tickets or add-ons) as ineligible for crypto payment. Scope: (1) one plugin setting — a
ModelMultipleChoiceFieldscoped toItem.objects.filter(event=event), stored via hierarkey; (2) enforce in/plugin/x402/purchase/(reject 400 if anytickets[]oraddons[]item is blocked — reject the whole cart, no per-line-item split); (3)payment.py.is_allowed(request, total, order)returns False when an order contains a blocked position so Pretix-native checkout automatically hides the method; (4) expose the blocklist in/api/x402/tickets/so the devcon store greys out the crypto button on blocked rows; (5) tests for blocked-only cart, mixed cart, allowed cart, native-checkout hiding. Decisions baked in: cart-level rejection (not split), item-level (not variation-level), voucher-agnostic. Estimated ~2-3h. - Bump-and-rebroadcast for a stuck relayer tx: when the relayer's
transferWithAuthorizationis underpriced for current network conditions, the tx sits in mempool and eventually drops. Buyer sees "verifying…" until the frontend's 90 s budget exhausts; the authorization is unconsumed (not a financial loss — funds never left the buyer's wallet — but a UX loss). The 25 %maxFeePerGasheadroom inrelayer.pycovers most spikes; this TODO is for the long tail. Scope: (1) on broadcast, persist(tx_hash, nonce, chain_id, broadcast_at)next to theX402PendingOrderso we can recover the relayer state; (2) periodic task every ~15 s checks each unsettled relayer tx — ifeth_getTransactionByHashreturnsNone(dropped) AND the nonce is still next-up on the relayer account, rebuild withmaxFeePerGas × 1.5and re-broadcast; ifeth_getTransactionReceiptexists, mark settled and let normal verify finalize; (3) cap retries at 3 to bound merchant gas spend per stuck order; (4) on final-give-up, mark the pending order with arelayer_failedflag so the buyer sees a clear "please retry — your funds were not charged" message and can re-initiate. Estimated ~120 lines, ~2-3 h focused work. Buyer's authorizationvalidBefore(default 10 min) caps the rescue window naturally. - Reorg monitoring (optimistic-accept, deferred-verify): when
min_confirmationsis set to 0 the buyer flow is fastest, but a 1-block reorg on Ethereum L1 (~0.05–0.1% of slots) or Polygon (a few small reorgs/day) could orphan a tx we accepted. Build a deferred-finality sweep so we get the UX of 0-confs and the safety of multi-conf. Scope: (1) addreorg_check_state('pending'|'safe'|'reorged'),reorg_checked_at,recorded_block_hashcolumns toX402CompletedOrder(one migration); (2) captureblock_hashat verify time alongsideblock_number; (3) periodic task (~60s cadence, register via the existingperiodic_tasksignal) re-fetches the receipt for anyreorg_check_state='pending'row past a per-chain delay (L2 rollups: 30s/1 conf, Ethereum L1: 180s/12 confs, Polygon: 60s/16 confs); (4) on intactblock_hash+ sufficient depth → marksafe; on differingblock_hash→ benign re-mine, update hash and stay pending one more cycle; on missing receipt → markreorged, call_cancel_order(send_mail=True), log WARNING; (5) admin UI badge on the completed table (⏳ verifyingwhile pending,🚩 reorged — order canceledon reorged rows) plus a filter chip; (6) require 2 consecutive RPC failures before declaringreorged(defensive against flaky providers); (7) tests covering happy path / re-mine / orphaned. Prereq: do this before lowering the globalmin_confirmationsfrom 1 to 0. Estimated ~200–250 lines, ~2-3h focused work. Decisions baked in: auto-cancel (not flag-only), no on-chain refund (a reorged tx by definition didn't pay us — the buyer's funds never left their wallet). - Admin manual verification for legacy Pretix-native checkout (wc_inject): there is currently no in-app recovery path for a stuck
WalletConnectPaymentpayment (Pretix order exists,OrderPayment(state='created')). Today it must be reconciled by an admin manually in Pretix's backend (shell / OrderPayment edit). Build a narrow admin endpoint that covers the common case: the user made it through the bundle's step 3 (quote creation) and broadcast the tx, but something failed before confirm. Scope: (1) show wc_inject pendings in the devcon admin pending table (queryOrderPayment.objects.filter(order__event=event, provider='walletconnect', state='created')alongsideX402PendingOrder, with asource: 'wc_inject'discriminator); (2)admin_verify_wc_injectendpoint keyed onorder_codethat readsOrderPayment.info_data['quote']and refuses if absent (out of scope — direct to Pretix native admin); (3) explicitly ignore the quote TTL in this endpoint — the tx'samount_rawwas locked at quote time, the user already paid that amount, so TTL-based price-drift checks don't apply; the admin is accepting any economic drift as part of the recovery; (4) re-runverify_native_eth/verify_erc20_transferagainst the quote'samount_raw+intended_payer+receive_address, thenWCPaymentAttempt.objects.create(...)atomically +payment.confirm(). No new ETH signature input needed — the challenge signature was already verified at quote-creation. Estimated ~2-3h, and will become dead code once the native-UI upgrade above lands, so worth deferring unless wc_inject stuck-payment volume is actually a pain.
Requires Python 3.10+ and Node 20+.
git clone https://github.com/efdevcon/pretix-eth-payment-plugin.git
cd pretix-eth-payment-plugin
pip install -e '.[dev]'
# Build frontend (WalletConnect checkout UI)
cd pretix_eth/static/wc_inject
pnpm install
pnpm run build # or: pnpm run watch
# Run tests
cd ../../..
pytest tests/ -vIt started with ligi suggesting pretix for Ethereum Magicians.
Then it was used for Ethereum Magicians in Paris (shout out to boris for making this possible) - but accepting ETH or DAI was a fully manual process there.
Afterwards boris put up some funds for a gitcoin bounty to make a plugin that automates this process. And nanexcool increased the funds and added the requirement for DAI.
The initial version was developed by vic-en but he vanished from the project after cashing in the bounty money and left the plugin in a non-working state.
Then the idea came up to use this plugin for DevCon5 and the plugin was forked to this repo and david sanders, piper merriam, rami, Pedro Gomes, and Jamie Pitts brought it to a state where it is usable for DevCon5 (still a lot of work to be done to make this a good plugin). Currently, it is semi-automatic. But it now has ERC-681 and Web3Modal support. If you want to dig a bit into the problems that emerged short before the launch you can have a look at this issue
For DEVcon6 the plugin was extended with some more features like Layer2 support by Rahul. Layer2 will play a significant role in Ethereum. Unfortunately DEVcon6 was delayed due to covid - but we where able to use and this way test via the LisCon ticket sale. As far as we know this was the first event ever offering a Layer2 payment option. In the process tooling like Web3Modal / Checkout that we depend on was improved.
For Devconnect IST an effort was made to improve the plugin in a variety of ways: WalletConnect support, single receiver mode (accept payments using just one wallet), more networks, automatic ETH rate fetching, improved UI and messaging, and smart contract wallet support. All of these features made it into this version of the plugin, except for smart contract wallet support - issues processing transactions stemming from sc wallets meant that we ultimately had to turn away sc wallet payments altogether.
For Devconnect 2025, the plugin was rewritten to use Daimo Pay, providing any-chain checkout and automatic refunds. See DIP-64.
For Devcon 8, the plugin was rebuilt from scratch by Didier Krux with two payment modes: direct WalletConnect checkout (user pays gas) and x402 gasless (relayer pays gas for stablecoins). All crypto logic now lives natively in the Pretix plugin — no external database (Supabase retired), no vendor dependency (Daimo Pay removed). Smart wallet support (ERC-1271, ERC-6492, ERC-4337) was added for both payment paths. The devcon-next frontend proxies to the plugin via Pretix API tokens.