You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Scope: crypto-to-fiat offramp endpoints in Gateway (new offramp trading type), backed by Peer on Base
Status: scoping, no code yet. This asks a scope question before any PR: is a
non-custodial crypto-to-fiat offramp something Gateway wants to host at all? If
the answer is no, that is a useful answer and I will not open the PR.
It proposes a fourth connector trading type, offramp, alongside router, amm, and clmm, following the same shape as the lending type scoped in #672. Backed by Peer (ZKP2P) on Base, which src/templates/chains/ethereum.yml already lists in defaultNetworks, so no
new chain implementation is involved.
1. The gap
Every connector in src/connectors/ converts one token into another token. A
strategy that accumulates USDC on Base has no path inside Gateway to turn that
USDC into bank-account fiat. Today an operator stops at Gateway, exports to a
centralized exchange, and finishes the leg by hand.
Peer closes that leg on-chain. It is a peer-to-peer offramp: the operator's USDC
becomes an escrow deposit, a counterparty pays them fiat off-chain (Venmo,
Revolut, Wise, Zelle, Cash App, PayPal), proves that payment with a TEE-TLS
attestation, and the escrow contract releases the USDC to the counterparty.
Nobody takes custody, and there is no offramp provider holding a float.
I looked at whether this fits the existing router contract before proposing a
new type. It does not, for four reasons, and I would rather name them than paper
over them.
SwapExecuteResponse.signature is required and means an on-chain
signature. An offramp has a transaction (the escrow deposit) but the
economically meaningful event, the fiat arriving, happens later and off-chain.
Reporting the deposit signature as if it were a completed swap would be wrong.
Settlement is asynchronous. A fill is minutes to hours, not one request. router assumes quote and execute bracket a single confirmation.
GET /chains/{chain}/poll resolves a transaction, not an order. There is
no route that answers "has the fiat landed".
There is no locked quote. Peer prices at the Chainlink oracle rate at fill
time with zero spread. QuoteSwapResponse.amountOut implies a committed
output that the protocol deliberately does not offer.
Forcing this into router would mean lying in signature and blocking execute-swap for an unbounded period. A separate trading type keeps the router
contract honest, which is the same reasoning #672 applies to lending.
3. What this does not need
Two things that would normally make an async flow a bad fit for Gateway do not
apply here, and both are worth stating up front.
No durable order store.src/services/quote-cache.ts is an in-memory Map
with no TTL or persistence, which is correct for a seconds-lived DEX quote and
would be wrong for a multi-hour order. Peer does not need it. An order's entire
state derives from its depositId plus indexed chain data, so the SDK resumes
any order from that id alone. A Gateway restart loses nothing. The depositId
is the durable handle, and the chain is the store.
No new signing model. Peer is non-custodial and never touches a private key. prepare() returns an unsigned txs[] with a same-index steps[] plan
(approve, createDeposit, withdrawDeposit). Gateway signs and sends those
through the existing wallet chokepoint, exactly as uniswap/router-routes/executeQuote.ts does with a locally built unsigned
transaction. The connector adds no key handling.
4. Endpoint surface (offramp trading type)
Per-connector /connectors/peer/offramp/*, plus unified /trading/offramp/*
mirroring /trading/amm/*.
Reads
Endpoint
Backing SDK verb
Returns
GET markets
capabilities(), fillStats()
supported platform and currency pairs, amount bounds, 30-day fill counts and median first-fill seconds per pair
GET quote-offramp
estimate({ amount, currency })
oracle rate, indicative receive amount, ETA { seconds, label }. Explicitly indicative, not a locked quote
GET order-status
order(depositId)
order state, gross deposited, filled, remaining, per-fill receipts
GET orders-owned
orders(owner)
every open order for a wallet
Writes (all return { signature, status, data? }, the existing shape,
plus depositId)
prepareWithdraw(depositId, amount?), the single unwind verb, partial with an amount or full close without
POST top-up-offramp
prepareTopUp(depositId, amount), adds USDC to a live order at the same payee and rate
Order state is a small closed set (awaiting-buyer, matched, delivering, delivered, returned), so order-status maps onto a status enum rather than
the 3-state on-chain one.
5. Schema sketch (src/schemas/offramp-schema.ts)
Connector-agnostic TypeBox, so a second offramp connector slots in without
reshaping:
QuoteOfframpResponse: { rate, receiveAmount, currency, kind: 'oracle-estimate', etaSeconds?, etaLabel? }. No minAmountOut, because there is no commitment to make.
Write response reuses { signature, status, data? } and adds depositId, which is the field callers poll on.
6. Architecture insertion points
Mirrors the file set every recent connector uses.
Create
src/schemas/offramp-schema.ts
src/connectors/peer/: peer.ts (singleton, getInstance(network), wraps createCashClient), peer.config.ts (tradingTypes=['offramp']), peer.routes.ts, schemas.ts, offramp-routes/ with one file per operation plus index.ts
src/trading/trading-offramp-routes/ plus common.ts
Client side. Gateway routes are usable directly over HTTP, but a strategy
that offramps needs a matching path in the Hummingbot client. Is a
Gateway-only connector acceptable as a first step?
Payee identity. Wise and PayPal need an identity attestation to register a
new payee. Peer accepts one but does not mint it. In practice an operator
registers the payee once on Peer's own surface and Gateway reuses the
registered handle. Confirming that is acceptable matters before any code.
Governance. If this needs a Hummingbot Governance Proposal rather than a
PR, say so now and I will route it that way.
Happy to prototype against Base with a small amount if there is appetite. If
there is not, closing this is the right call and costs nobody a review cycle.
Scope: crypto-to-fiat offramp endpoints in Gateway (new
offramptrading type), backed by Peer on BaseStatus: scoping, no code yet. This asks a scope question before any PR: is a
non-custodial crypto-to-fiat offramp something Gateway wants to host at all? If
the answer is no, that is a useful answer and I will not open the PR.
It proposes a fourth connector trading type,
offramp, alongsiderouter,amm, andclmm, following the same shape as thelendingtype scoped in#672. Backed by Peer (ZKP2P) on Base, which
src/templates/chains/ethereum.ymlalready lists indefaultNetworks, so nonew chain implementation is involved.
1. The gap
Every connector in
src/connectors/converts one token into another token. Astrategy that accumulates USDC on Base has no path inside Gateway to turn that
USDC into bank-account fiat. Today an operator stops at Gateway, exports to a
centralized exchange, and finishes the leg by hand.
Peer closes that leg on-chain. It is a peer-to-peer offramp: the operator's USDC
becomes an escrow deposit, a counterparty pays them fiat off-chain (Venmo,
Revolut, Wise, Zelle, Cash App, PayPal), proves that payment with a TEE-TLS
attestation, and the escrow contract releases the USDC to the counterparty.
Nobody takes custody, and there is no offramp provider holding a float.
Live contracts on Base (chain id 8453):
0x777777779d229cdF3110e9de47943791c26300Ef0x014025fDE093f8701d86e9f38e2C3a9b779cb5c70x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913SDK:
@zkp2p/cash(TypeScript,viem peer dep, Apache-compatible), docs at
https://docs.peer.xyz/developer/peer-cash.
2. Why this is not a
routerconnectorI looked at whether this fits the existing router contract before proposing a
new type. It does not, for four reasons, and I would rather name them than paper
over them.
SwapExecuteResponse.signatureis required and means an on-chainsignature. An offramp has a transaction (the escrow deposit) but the
economically meaningful event, the fiat arriving, happens later and off-chain.
Reporting the deposit signature as if it were a completed swap would be wrong.
routerassumes quote and execute bracket a single confirmation.GET /chains/{chain}/pollresolves a transaction, not an order. There isno route that answers "has the fiat landed".
time with zero spread.
QuoteSwapResponse.amountOutimplies a committedoutput that the protocol deliberately does not offer.
Forcing this into
routerwould mean lying insignatureand blockingexecute-swapfor an unbounded period. A separate trading type keeps the routercontract honest, which is the same reasoning #672 applies to
lending.3. What this does not need
Two things that would normally make an async flow a bad fit for Gateway do not
apply here, and both are worth stating up front.
No durable order store.
src/services/quote-cache.tsis an in-memoryMapwith no TTL or persistence, which is correct for a seconds-lived DEX quote and
would be wrong for a multi-hour order. Peer does not need it. An order's entire
state derives from its
depositIdplus indexed chain data, so the SDK resumesany order from that id alone. A Gateway restart loses nothing. The
depositIdis the durable handle, and the chain is the store.
No new signing model. Peer is non-custodial and never touches a private key.
prepare()returns an unsignedtxs[]with a same-indexsteps[]plan(
approve,createDeposit,withdrawDeposit). Gateway signs and sends thosethrough the existing wallet chokepoint, exactly as
uniswap/router-routes/executeQuote.tsdoes with a locally built unsignedtransaction. The connector adds no key handling.
4. Endpoint surface (
offramptrading type)Per-connector
/connectors/peer/offramp/*, plus unified/trading/offramp/*mirroring
/trading/amm/*.Reads
GET marketscapabilities(),fillStats()GET quote-offrampestimate({ amount, currency }){ seconds, label }. Explicitly indicative, not a locked quoteGET order-statusorder(depositId)GET orders-ownedorders(owner)Writes (all return
{ signature, status, data? }, the existing shape,plus
depositId)POST execute-offrampprepare(input)returns unsignedtxs[]; Gateway signs and sends;finalizePreparedCashout(receipt)resolvesdepositIdPOST withdraw-offrampprepareWithdraw(depositId, amount?), the single unwind verb, partial with an amount or full close withoutPOST top-up-offrampprepareTopUp(depositId, amount), adds USDC to a live order at the same payee and rateOrder state is a small closed set (
awaiting-buyer,matched,delivering,delivered,returned), soorder-statusmaps onto a status enum rather thanthe 3-state on-chain one.
5. Schema sketch (
src/schemas/offramp-schema.ts)Connector-agnostic TypeBox, so a second offramp connector slots in without
reshaping:
OfframpMarket:{ platform, currencies[], minAmount, maxAmount, fills30d, medianFillSeconds? }QuoteOfframpResponse:{ rate, receiveAmount, currency, kind: 'oracle-estimate', etaSeconds?, etaLabel? }. NominAmountOut, because there is no commitment to make.OfframpOrder:{ depositId, state, token, amountDeposited, amountFilled, amountRemaining, payoutLegs[], fills[] }ExecuteOfframpRequest:{ network, walletAddress, amount, receive: { platform, currency|currencies[], payee } }{ signature, status, data? }and addsdepositId, which is the field callers poll on.6. Architecture insertion points
Mirrors the file set every recent connector uses.
Create
src/schemas/offramp-schema.tssrc/connectors/peer/:peer.ts(singleton,getInstance(network), wrapscreateCashClient),peer.config.ts(tradingTypes=['offramp']),peer.routes.ts,schemas.ts,offramp-routes/with one file per operation plusindex.tssrc/trading/trading-offramp-routes/pluscommon.tssrc/templates/connectors/peer.yml,src/templates/namespace/peer-schema.jsonEdit
src/app.ts: import routes, swagger tag,app.registerat/connectors/peer/offrampsrc/config/routes/getConnectors.ts: append toconnectorsConfigsrc/templates/root.yml:$namespaceblocksrc/trading/swap/{quote,execute}.tsare untouched, because this does notregister as a
swapProvider.7. Open questions, which are the actual reason for this issue
clear no is a fine outcome.
lending(Proposal: add Jupiter Lend lending/borrowing/looping endpoints (newlendingtrading type) #672)? Iflendingestablishes the pattern for adding a type, this should follow itrather than invent a parallel one.
that offramps needs a matching path in the Hummingbot client. Is a
Gateway-only connector acceptable as a first step?
new payee. Peer accepts one but does not mint it. In practice an operator
registers the payee once on Peer's own surface and Gateway reuses the
registered handle. Confirming that is acceptable matters before any code.
PR, say so now and I will route it that way.
Happy to prototype against Base with a small amount if there is appetite. If
there is not, closing this is the right call and costs nobody a review cycle.