High-level description
Peer (formerly ZKP2P) is a non-custodial peer-to-peer fiat offramp on Base. A
maker escrows USDC and advertises the fiat rails they accept. A taker signals an
intent against that deposit, pays the maker off-chain (Venmo, Revolut, Wise,
Zelle, Cash App, PayPal), proves the payment with a TEE-TLS attestation, and the
escrow releases the USDC. There is no custodian and no offramp provider.
I would like to add five project-level spells covering the full lifecycle:
| Model |
Grain |
peer_base.deposits |
maker liquidity created and topped up |
peer_base.intents |
takers claiming liquidity, with the binding fiat rate |
peer_base.fills |
settled intents, gross and net, with the fee split |
peer_base.withdrawals |
maker liquidity leaving escrow |
peer_base.fees |
one row per non-zero fee earned per fill |
Which sub-project
daily_spellbook, as project-level spells at
dbt_subprojects/daily_spellbook/models/_projects/peer/base/.
Not dex and not perpetual. This is not an AMM or aggregator swap and it does
not fit dex.trades: the counterparty is another person, one side of the trade
settles off-chain in fiat, and settlement is asynchronous over minutes to hours.
There is no existing sector for P2P or payments, which is part of what I am
asking about below.
Why it would benefit the community
Peer contracts have been live on Base since January 2025 across three escrow
generations. Sampling blocks 50047215 to 50087215, about 22 hours, gives 277
intents signalled, 117 filled, and 35578 USDC of gross filled volume. Today none
of that is queryable on Dune without hand-decoding raw logs,
so the on-chain fiat-offramp category is effectively invisible. These spells make
tractable a set of questions nobody can currently answer: real fiat corridor
volume by rail and currency, maker capital efficiency and redeployment, taker
fill latency, and the actual take rate of a non-custodial offramp versus a
centralized one.
Contracts and events
Base mainnet, chain id 8453. Three escrow generations and three orchestrator
generations are live, and the models cover all of them.
| Role |
Address |
Live from |
| Escrow v2 |
0xCA38607D85E8F6294Dc10728669605E6664C2D70 |
2025-01-20 |
| Escrow v2.1 |
0x2f121CDDCA6d652f35e8B3E560f9760898888888 |
2025-10-25 |
| Escrow v2.2 |
0x777777779d229cdF3110e9de47943791c26300Ef |
2026-03-11 |
| Orchestrator v2.1 |
0x88888883Ed048FF0a415271B28b2F52d431810D0 |
|
| Orchestrator v2.2 |
0x888888359E981B5225CA48fbCdCeff702FC3b888 |
|
| Orchestrator v3 |
0x014025fDE093f8701d86e9f38e2C3a9b779cb5c7 |
|
Events consumed, with topic0 as computed and then confirmed against live Base
logs:
DepositReceived v2 0x68a835da25522a6767ad280764ce2daed02507359a889e4a18219458d2f356b4
DepositReceived v2.1+ 0x1236dbdc184b6c8721974cce53dabb6018679bca9a43784ab2ad71bcdb1d7dd1
DepositFundsAdded 0x6dd055b2e2bf3452b033736068d30d709ffb26f2016b48ba9a612c3701bf33a9
DepositWithdrawn 0xae1f357660ab777dcfd38c0ab6357834684ec26289ecfa07ec65dbf6c3c64312
DepositClosed 0x8ac07cc6e38c6222dd0309c80353c1962354bacf222b825d7401cc80e93ff3cc
IntentSignaled v2 0xc2a37468e454b839a2cff965f5933f675c886d1304fa1d29ce661503166874f5
IntentSignaled v2.1+ 0xf8c114f83581b2cf0b9f130782a93024aa8933e7d188901156bd68bdd558a20a
IntentFulfilled v2 0xa14478886b7c7d8bcb5da958a490bef7c9ef76654d2f4b65a04d94533bf25dc9
IntentFulfilled v2.1+ 0xd50b3b21bc45b85ddfaec58dbf56fe9b88754d08f47dcf5143b63258a57ad944
FundsUnlockedAndTransferred 0x45625e0810f65b3c601b5c91bdacdf9e8f9ec7098fce927c57a8a65a823fd617
IntentReferralFeeDistributed 0xa8441469d1604eb6b95bc9c00891d08bbf58a103b4d67ece28c885d98d0f8134
IntentManagerFeeSnapshotted 0x3f418918906a7be197ce7621b0e3d6848f86d2c97ddbc2bc7ac3568c5eea8b0b
Payment rail and currency are bytes32 on-chain: keccak256 of the lowercase
platform slug and of the ISO 4217 code respectively. The models resolve both back
to labels. Over a 30k block sample every observed rail hash (venmo, cashapp,
wise, revolut, paypal, zelle, chime, mercadopago) and every observed currency
hash (USD, EUR, GBP, CAD, AUD, ARS) resolved, with no unmapped values.
Two correctness traps worth flagging up front
These are the reasons I would rather agree the approach with you than guess.
1. IntentFulfilled.amount is net, not gross. From v2.1 that field is what
reached the taker after fees. Summing it as volume understates the protocol by
the entire fee take. Gross is FundsUnlockedAndTransferred.transferredAmount.
2. IntentManagerFeeSnapshotted.fee is a rate, not an amount. It is 1e18
fixed point, snapshotted at signal time, in a different block from the fill.
IntentReferralFeeDistributed.feeAmount next to it is an absolute amount in
6-decimal USDC base units. Adding the two into one fee column is wrong by twelve
orders of magnitude. The realised manager fee is
floor(amount_gross * rate / 1e18).
I validated the resulting identity against live Base logs over blocks
50047215 to 50087215:
signaled=277 fulfilled=117 referral_events=117 manager_snapshots=277
amount_gross == amount_net + referral_fee + floor(amount_gross * mgr_rate / 1e18)
ok=117 mismatch=0
gross=35578.409753 USDC referral=1332.929767 manager=0.679625 take=3.7484%
Worked single case, intent
0x00b793f4fc1da44d108d9af2c865813d6172d5b25490d588bffa761b37528cd8:
gross 65240205, referral 4566814, manager rate 1000000000000000 giving 65240,
and 65240205 - 4566814 - 65240 = 60608151, exactly IntentFulfilled.amount.
Decoded tables or raw logs
The Peer contracts are not submitted to Dune's decoder, so peer_base.*_evt_*
does not exist and the draft decodes source('base', 'logs') by topic0 with
fixed ABI offsets, following the pattern in
leverup_monad_perpetual_trades.sql.
This is the main thing I would like your call on. Options as I see them:
- Submit the escrow and orchestrator ABIs at
dune.com/contracts/new, wait for
the backfill, and rewrite the models onto decoded tables. Cleaner, and I am
happy to do it. It does mean six contracts across three generations.
- Keep the raw-log decode. The ABI layouts are fixed and already deployed, and
the topic0 filters are selective, but I appreciate it is not your default.
I will do whichever you prefer.
State of the draft
The five models and their _schema.yml are written. Against main at 21ac0dc:
uv run dbt --warn-error compile EXIT=0
Found 2379 models, 2737 data tests, 47 seeds, 9762 sources, 823 macros
(2374 models before, so the five new ones parse and compile)
Each model is incremental on delta with merge,
incremental_predicate('DBT_INTERNAL_DEST.block_time'), an explicit
schema/alias, and filtering_columns. Every model carries a
dbt_utils.unique_combination_of_columns matching its unique_key plus
not_null on the key columns, which is 5 uniqueness tests and 30 not_null
tests. check-script-ref-and-source, check-model-has-description and
check-model-has-properties-file pass.
I have deliberately not opened a PR, since the README asks for this issue first.
Say the word and I will open it, in whichever of the two shapes above you prefer.
Open questions
- Raw logs now, or decoded contracts first (see above)?
- Are project-level spells under
_projects/peer/base/ the right home, or would
you rather this became the start of a payments or P2P sector?
- Should
fills also feed any existing aggregate, or stay standalone? It is not
a dex.trades shape, but I want to check rather than assume.
- Seed file:
docs/seeds/seed_overview.md calls seeds encouraged rather than
required for standalone project spells. Happy to add one if you want it.
High-level description
Peer (formerly ZKP2P) is a non-custodial peer-to-peer fiat offramp on Base. A
maker escrows USDC and advertises the fiat rails they accept. A taker signals an
intent against that deposit, pays the maker off-chain (Venmo, Revolut, Wise,
Zelle, Cash App, PayPal), proves the payment with a TEE-TLS attestation, and the
escrow releases the USDC. There is no custodian and no offramp provider.
I would like to add five project-level spells covering the full lifecycle:
peer_base.depositspeer_base.intentspeer_base.fillspeer_base.withdrawalspeer_base.feesWhich sub-project
daily_spellbook, as project-level spells atdbt_subprojects/daily_spellbook/models/_projects/peer/base/.Not
dexand notperpetual. This is not an AMM or aggregator swap and it doesnot fit
dex.trades: the counterparty is another person, one side of the tradesettles off-chain in fiat, and settlement is asynchronous over minutes to hours.
There is no existing sector for P2P or payments, which is part of what I am
asking about below.
Why it would benefit the community
Peer contracts have been live on Base since January 2025 across three escrow
generations. Sampling blocks 50047215 to 50087215, about 22 hours, gives 277
intents signalled, 117 filled, and 35578 USDC of gross filled volume. Today none
of that is queryable on Dune without hand-decoding raw logs,
so the on-chain fiat-offramp category is effectively invisible. These spells make
tractable a set of questions nobody can currently answer: real fiat corridor
volume by rail and currency, maker capital efficiency and redeployment, taker
fill latency, and the actual take rate of a non-custodial offramp versus a
centralized one.
Contracts and events
Base mainnet, chain id 8453. Three escrow generations and three orchestrator
generations are live, and the models cover all of them.
0xCA38607D85E8F6294Dc10728669605E6664C2D700x2f121CDDCA6d652f35e8B3E560f97608988888880x777777779d229cdF3110e9de47943791c26300Ef0x88888883Ed048FF0a415271B28b2F52d431810D00x888888359E981B5225CA48fbCdCeff702FC3b8880x014025fDE093f8701d86e9f38e2C3a9b779cb5c7Events consumed, with topic0 as computed and then confirmed against live Base
logs:
Payment rail and currency are
bytes32on-chain:keccak256of the lowercaseplatform slug and of the ISO 4217 code respectively. The models resolve both back
to labels. Over a 30k block sample every observed rail hash (venmo, cashapp,
wise, revolut, paypal, zelle, chime, mercadopago) and every observed currency
hash (USD, EUR, GBP, CAD, AUD, ARS) resolved, with no unmapped values.
Two correctness traps worth flagging up front
These are the reasons I would rather agree the approach with you than guess.
1.
IntentFulfilled.amountis net, not gross. From v2.1 that field is whatreached the taker after fees. Summing it as volume understates the protocol by
the entire fee take. Gross is
FundsUnlockedAndTransferred.transferredAmount.2.
IntentManagerFeeSnapshotted.feeis a rate, not an amount. It is 1e18fixed point, snapshotted at signal time, in a different block from the fill.
IntentReferralFeeDistributed.feeAmountnext to it is an absolute amount in6-decimal USDC base units. Adding the two into one fee column is wrong by twelve
orders of magnitude. The realised manager fee is
floor(amount_gross * rate / 1e18).I validated the resulting identity against live Base logs over blocks
50047215 to 50087215:
Worked single case, intent
0x00b793f4fc1da44d108d9af2c865813d6172d5b25490d588bffa761b37528cd8:gross 65240205, referral 4566814, manager rate 1000000000000000 giving 65240,
and 65240205 - 4566814 - 65240 = 60608151, exactly
IntentFulfilled.amount.Decoded tables or raw logs
The Peer contracts are not submitted to Dune's decoder, so
peer_base.*_evt_*does not exist and the draft decodes
source('base', 'logs')by topic0 withfixed ABI offsets, following the pattern in
leverup_monad_perpetual_trades.sql.This is the main thing I would like your call on. Options as I see them:
dune.com/contracts/new, wait forthe backfill, and rewrite the models onto decoded tables. Cleaner, and I am
happy to do it. It does mean six contracts across three generations.
the topic0 filters are selective, but I appreciate it is not your default.
I will do whichever you prefer.
State of the draft
The five models and their
_schema.ymlare written. Againstmainat21ac0dc:Each model is
incrementalondeltawithmerge,incremental_predicate('DBT_INTERNAL_DEST.block_time'), an explicitschema/alias, andfiltering_columns. Every model carries adbt_utils.unique_combination_of_columnsmatching itsunique_keyplusnot_nullon the key columns, which is 5 uniqueness tests and 30not_nulltests.
check-script-ref-and-source,check-model-has-descriptionandcheck-model-has-properties-filepass.I have deliberately not opened a PR, since the README asks for this issue first.
Say the word and I will open it, in whichever of the two shapes above you prefer.
Open questions
_projects/peer/base/the right home, or wouldyou rather this became the start of a payments or P2P sector?
fillsalso feed any existing aggregate, or stay standalone? It is nota
dex.tradesshape, but I want to check rather than assume.docs/seeds/seed_overview.mdcalls seeds encouraged rather thanrequired for standalone project spells. Happy to add one if you want it.