Skip to content

Busha liquidity pilot: temporary API adapter

This is Plan A: Textile asks Busha for an address-to-address crypto conversion quote, compares it with a native Textile RFQ, and routes the whole request to one source. Busha supplies its shared operating inventory, not a dedicated Textile EOA, OperatorVault, or Stitch bot.

The native route still executes through Textile contracts. The Busha route uses Textile's API for quote ownership, transfer creation, reconciliation and status, but does not settle through Textile contracts. That is Plan A's main compromise. It cannot offer atomic execution, contract-enforced output, or the native reactor fee. User funds are exposed to Busha between deposit and payout. Execution speed, late deposits and refunds need separate operating terms.

LinkNew quote requests
/s/swapTextile only. No Busha calls or Busha recovery polling.
/s/swap?bushaLiquidity=1Compare Textile and Busha; select the higher net output within the same gross sell budget.
/s/swap?bushaOnly=1Busha only. No native quote, preview or fallback.

bushaOnly=1 enables the pilot on its own and takes precedence if both flags are present. Only the literal value 1 opts in. The choice is not saved as a user preference. Append the flags to existing sell, buy, chainId and amount links; the existing trade URL writer preserves foreign parameters.

For example, BSC USDT → cNGN:

text
/s/swap?chainId=56&sell=USDT&buy=cNGN&amount=12&bushaLiquidity=1

This pilot allows both directions of cNGN/USDT on BSC and cNGN/USDC on Base. Tokens must match Textile's canonical address registry, and the corridor must also be listed in the database. No fiat NGN leg, cross-chain conversion, customer creation, balance-prefunding, or fallback to business balances. Busha must enable the requested pair and both external payment networks on our account; public currency metadata alone is not proof of that.

Before wallet connection, the panel requests an indicative Busha price through POST /preview. This uses Busha's unauthenticated quote endpoint, then deducts the target network's published withdrawal fee. It does not create a business quote, store a trade, reserve liquidity, or invent a payout wallet. Final merchant pricing and fees are confirmed by the authenticated firm quote. Mixed mode compares both estimates; Busha-only never calls native preview.

Firm requests are made on click, not while typing. Both sources receive the same atomic sell budget. Net output already includes each route's fees; the adapter does not subtract Busha's fee line a second time. Textile wins ties. Gas and delivery time are disclosed separately, not converted into a guessed ranking penalty. This is not a split-fill optimizer. A losing Textile quote is released. Unsupported/unavailable Busha liquidity leaves Textile eligible in mixed mode; Busha-only fails closed. Maker preferred/restricted steering cannot be combined with this pilot.

Execution and money

text
Request quote → compare candidates → review the selected route
                                      ├ Textile: existing approval + contract swap
                                      └ Busha: accept → verify payment instructions
                                                  → confirm ERC20 payment in wallet
                                                  → Busha converts and pays wallet

“Accept and pay with wallet” opens the connected quoting wallet for an ERC20 transfer of the exact sell amount to Busha's verified per-transfer address. No spender approval is needed. The client refreshes the claimed transfer and checks the reviewed terms, canonical token, balance, wallet, network and payment deadline before sending. The page shows the payout wallet, amount, network, expiry and non-atomic settlement warning before acceptance. Sandbox payments are blocked.

An atomic browser Web Lock prevents simultaneous sends from tabs in the same browser. A saved payment-attempt marker blocks another send after a refresh or uncertain wallet response; only a structured wallet rejection clears it for retry. Browsers without Web Locks cannot pay. Safe signers must approve and execute before the deadline. An on-chain receipt confirms the deposit transaction, not Busha's payout. Keep the browser recovery data until settlement; it does not protect against payments from other browsers or manual transfers. The old “I have sent the payment” acknowledgement remains for users who already funded manually. It blocks another wallet payment but is not proof that Busha received funds.

“Cancel swap” discards an unaccepted quote using the existing atomic discard endpoint. For an accepted, unpaid transfer, “Cancel swap flow” asks the user to confirm that no wallet or browser has submitted payment. It refreshes Busha status, takes the same cross-tab lock as payment, blocks further browser funding of that transfer, and returns to quotes while retaining the claim under “Check saved Busha transfer”. This is local abandonment, not provider cancellation. Late receipts can still settle; the recovery view continues to show Busha's real status. Cancellation is blocked during wallet submission, after any saved payment attempt or manual acknowledgement, or when status cannot be verified. Reject an unsigned request in the wallet itself; submitted payments cannot be cancelled from this page.

Busha earns its agreed trading margin/fees in its all-in quote. Textile does not add a fee to the Busha route in this version. Native Textile fees remain unchanged. An agreed rebate/invoice arrangement is possible outside this code; native Busha partner fee splitting was reported unavailable. Do not market this pilot as collecting a Textile protocol fee on Busha transfers.

Quotes and transfers live in busha_liquidity_trade, not RfqRequest or RampTransfer. They must not be counted as native on-chain fills, points, referrals or maker performance. There are no new writes to those systems.

Enable and verify

The server is enabled by default, including when BUSHA_LIQUIDITY_ENABLED is unset. Set it to false to disable new quotes and accepts. The web still requires one of the URL flags above; ordinary swap visits remain Textile-only. The link is a discovery gate, not authorization. The API still requires wallet proof, rate limiting and a private claim for lifecycle calls. Partner API keys are deliberately excluded from this wallet-only pilot so TEST credentials cannot book live business-account transfers.

Apply the additive Prisma migration and regenerate the Prisma client before running the new code. Reuse BUSHA_SECRET_KEY on the API/RFQ service serving /v2; missing credentials prevent firm quotes. Public estimates need no secret. The liquidity adapter deliberately omits X-BU-PUBLIC-KEY on business quotes. Sending our own ramp merchant key with our business bearer token makes Busha reject the quote with "Self transfer is not allowed". Keep BUSHA_PUBLIC_KEY configured for the existing ramp; do not remove it from the shared client or deployment to fix liquidity quotes. If an existing deployment explicitly sets BUSHA_LIQUIDITY_ENABLED=false, remove that override or set it to true to enable the integration. The browser never receives provider credentials.

For a sandbox deployment set BUSHA_LIQUIDITY_SANDBOX=true and configure BUSHA_SANDBOX_SECRET_KEY / BUSHA_SANDBOX_PUBLIC_KEY. The shared client swaps credentials and host together. Sandbox is refused when ENVIRONMENT=production. The environment is persisted per trade so changing the new-quote setting does not move an existing trade to another provider account.

Before enabling real deposits, use Busha's tech call and sandbox to confirm:

  1. Both directions work as external crypto deposit → conversion → external crypto payout under our business account, including the exact supported token addresses and networks. Confirm the user/compliance obligations for this flow; no customer API usage is not a general KYC exemption.
  2. target_amount is the final amount arriving in the recipient wallet after all provider/withdrawal fees. Quote and transfer amounts must be identical.
  3. What the expiry binds: quote acceptance, deposit broadcast, receipt or confirmations. Confirm late, insufficient and excess deposits, refunds, payout failures and who contacts the user. The two-minute UI buffer is a conservative stop, not a guarantee of timely confirmation.
  4. The transfer response includes verifiable quote_id, currencies, amounts, and address/network/type on both payment legs. Confirm quote-ID lookup after a lost POST response, initial deposit-ready status, polling allowance, normal delivery times and an escalation contact.

Use small real trades only after those checks, with explicit operator approval. This PR does not enable a deployment or execute any live Busha transfer.

API and recovery

All paths are under /v2/liquidity/busha; all responses are non-cacheable and use { data: ... } / the existing API error envelope.

Method and pathContract
POST /previewchainId, canonical sellToken/buyToken, atomic sellAmount. Returns indicative atomic sellAmount and buyAmount. No wallet or proof; IP rate limiting still applies.
POST /quoteschainId, canonical sellToken/buyToken, atomic sellAmount, taker, signed takerProof. Returns trade terms and a private claimToken. Does not create a transfer.
POST /:id/acceptX-Busha-Claim and { "acceptNonAtomicSettlement": true }. Creates the provider transfer at most once.
GET /:idX-Busha-Claim. Returns terms/status; reconciles accepted trades with Busha, at most one poll per five seconds per trade across API instances.
POST /:id/discardX-Busha-Claim. Atomically discards an unaccepted quote. Refused if transfer creation already started. Never cancels a funded transfer.

Only the claim hash is stored in the database. Each browser trade has a separate local-storage recovery record saved before accept. A refresh with the same wallet and either opt-in link resumes it. Do not clear browser data while pending. Keep the displayed trade ID for support. A lost browser claim requires manual operator verification and reconciliation; this pilot does not implement email recovery or a self-service claim reissue endpoint.

Provider quote refusals return a readable 409 instead of an opaque 500; provider throttling returns 429 and outages return 502. Unexpected provider details stay in server logs. In production, /v2 runs in the dedicated RFQ service: look up the response request ID in /ecs/textile-production/rfq, not the general API log group.

An accepted transfer still pending ten minutes after its payment deadline is shown as payment_expired. This is a recovery view, not a provider cancellation: the database stays pending and late receipts can still reconcile. Users can return to quotes while retaining the claim, then use “Check saved Busha transfer” to resume status checks. Dismissed transfers aren't polled in the background. If payment was already sent, check the saved transfer or contact support before trading again. Known reverse_fund_conversion updates remain in-progress refunds.

Native broadcasts stay recoverable past quote expiry. Failed or expired native swaps unblock new quotes only once the RFQ status API reports lateFillCorrectionClosed: true, using the server's existing 24-hour correction window. Missing flags from older APIs keep recovery active. This additive status field and the native recovery fixes can remain after removing the Busha pilot.

Transfer creation is one-way quoted → creating in the database before calling Busha. Concurrent accepts cannot both POST. A crash/timeout remains creating until GET finds the exact provider quote's transfer. An empty lookup is not permission to retry POST. A definitive create rejection can also need manual review: this deliberately trades automatic retry convenience for avoiding duplicate transfers. Operators must verify Busha's history before any repair. Provider terms are rechecked before wallet payment. Status updates use compare-and-set and cannot reopen payment instructions after receipt. Expired trades are never silently repriced or automatically routed elsewhere.

No background worker or existing ramp webhook is changed. Reconciliation is lazy while the status page is open; Busha continues processing when it closes. For pilot operations, review pending/creating/needs_review records against Busha's dashboard and the provider quote/reference. Do not interpret browser inactivity or an old database status as a failed payment. A provider webhook or dedicated scheduled reconciler is the next improvement before broad rollout.

Removal without touching native operations

First set BUSHA_LIQUIDITY_ENABLED=false. This blocks new Busha quotes and unaccepted transfers, while GET and reconciliation of started transfers remain available. Remove opt-in links from distribution, but keep working recovery links for existing users. Do not remove the API, secrets or recovery UI while any accepted trade is unresolved.

After reconciling every accepted trade and archiving the required records, replace LiquidityPanel with RfqPanel in TradeSwapExperience.tsx, remove the single registerBushaLiquidity registration/import in api/src/v2/plugin.ts, and remove web/src/features/bushaLiquidity/ and api/src/lib/bushaLiquidity/. Remove the two environment settings. The generic optional RfqPanel transport seam can stay for a replacement provider or be removed separately; its default is always the existing native implementation. The exported HTTP helper in rfqApi.ts is also safe to leave.

Keep the additive migration history. Drop/archive the isolated table only in a later explicit migration after retention and reconciliation, never as part of the feature kill switch. No native RFQ, fiat ramp, contract, vault, maker, ledger or existing table needs deletion or data migration.

The replacement can implement Busha-signed RFQs and just-in-time native settlement behind the same UI entry seam. That is the more elegant path, but it needs a separate capital/settlement agreement and cannot be simulated by wrapping an API quote in a fake signed Textile order.

Reference: Busha's official OpenAPI specification. The reported firm-quote and commercial terms came from our Busha conversation; they still need the execution checks above.