Skip to content

Proposal: crypto-to-fiat offramp endpoints in Gateway (new offramp trading type), backed by Peer on Base #681

Description

@ADWilkinson

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.

Live contracts on Base (chain id 8453):

Contract Address
EscrowV2 0x777777779d229cdF3110e9de47943791c26300Ef
OrchestratorV3 0x014025fDE093f8701d86e9f38e2C3a9b779cb5c7
USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913

SDK: @zkp2p/cash (TypeScript,
viem peer dep, Apache-compatible), docs at
https://docs.peer.xyz/developer/peer-cash.

2. Why this is not a router connector

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.

  1. 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.
  2. Settlement is asynchronous. A fill is minutes to hours, not one request.
    router assumes quote and execute bracket a single confirmation.
  3. GET /chains/{chain}/poll resolves a transaction, not an order. There is
    no route that answers "has the fiat landed".
  4. 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)

Endpoint Backing
POST execute-offramp prepare(input) returns unsigned txs[]; Gateway signs and sends; finalizePreparedCashout(receipt) resolves depositId
POST withdraw-offramp 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:

  • OfframpMarket: { platform, currencies[], minAmount, maxAmount, fills30d, medianFillSeconds? }
  • QuoteOfframpResponse: { rate, receiveAmount, currency, kind: 'oracle-estimate', etaSeconds?, etaLabel? }. No minAmountOut, 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 } }
  • 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
  • src/templates/connectors/peer.yml, src/templates/namespace/peer-schema.json

Edit

  • src/app.ts: import routes, swagger tag, app.register at /connectors/peer/offramp
  • src/config/routes/getConnectors.ts: append to connectorsConfig
  • src/templates/root.yml: $namespace block

src/trading/swap/{quote,execute}.ts are untouched, because this does not
register as a swapProvider.

7. Open questions, which are the actual reason for this issue

  1. Does Gateway want a fiat leg at all? This changes what Gateway is for. A
    clear no is a fine outcome.
  2. Does a fourth trading type land before or after lending (Proposal: add Jupiter Lend lending/borrowing/looping endpoints (new lending trading type) #672)? If
    lending establishes the pattern for adding a type, this should follow it
    rather than invent a parallel one.
  3. 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?
  4. 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.
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions