@bufinance/pasillo-client
v0.1.0
Published
Typed TypeScript client for the Pasillo B2B API — ramps, intraday FX (StableFX + Telarana), customers, settle. ESM-only, zero runtime deps beyond global fetch + crypto.randomUUID. Pair with an EIP-712-capable wallet (viem, ethers, etc.) for FX trade signi
Maintainers
Readme
@bufinance/pasillo-client
Typed TypeScript client for Pasillo — BU.FI's B2B API gateway for stablecoin ramps, intraday FX (Circle StableFX + BU.FI Telarana), and onchain settlement.
ESM-only. Zero runtime dependencies (fetch + crypto.randomUUID are globals on every modern runtime: Node 18+, Bun, Deno, Cloudflare Workers, browsers).
npm i @bufinance/pasillo-client
# or: bun add @bufinance/pasillo-clientQuickstart
import { createPasilloClient } from '@bufinance/pasillo-client';
const pasillo = createPasilloClient({
baseUrl: 'https://api.pasillo.bufi.io', // or http://localhost:3003 for dev
apiKey: process.env.PASILLO_API_KEY!, // mint from b-in-bufi admin UI
customerId: process.env.PASILLO_CUSTOMER_ID, // optional; required for /fx/trade
});
// 1. Request a USDC → EURC quote (atomic mode by default)
const quote = await pasillo.fx.quote({
from: { currency: 'USDC', amount: '1000' },
to: { currency: 'EURC' },
tenor: 'instant',
recipientAddress: '0xYourEvmAddress',
});
// 2. Sign quote.signingRequest.typedData with your wallet
// (viem, ethers, MetaMask, WaaS — bring your own EIP-712 signer)
const signature = await wallet.signTypedData(quote.signingRequest.typedData);
// 3. Submit the signed trade
const trade = await pasillo.fx.trade.create({
providerQuoteId: quote.providerQuoteId,
pair: { base: 'USDC', quote: 'EURC' },
callerAddress: '0xYourEvmAddress',
signedPayload: { message: quote.signingRequest.typedData.message, signature },
quoteSnapshot: quote,
});
// 4. Poll
const latest = await pasillo.fx.trade.get(trade.id);
console.log(latest.status); // 'submitted' → 'funding' → 'funded' → 'settled'Atomic vs bonded mode
Pasillo defaults to atomic mode — the EIP-712 payload's permitted.amount is rewritten to equal the full quote amount, so recordTrade pulls the full sell-side notional in one shot. No risk buffer, no follow-up takerDeliver call required.
If you need bonded mode (advanced), pass acceptBondedRisk: true on the quote:
const bonded = await pasillo.fx.quote({
from: { currency: 'USDC', amount: '1000' },
to: { currency: 'EURC' },
tenor: 'instant',
recipientAddress: '0xYourEvmAddress',
acceptBondedRisk: true,
});
console.log(bonded.fundingMode); // 'bonded'
console.log(bonded.permittedAmount); // small bond pulled at recordTrade
console.log(bonded.quoteAmount); // full amount you owe by maturity
console.log(bonded.worstCaseLoss); // forfeit on breach
console.log(bonded.followupDeliverBy); // deadline for fx.trade.fund(...)
console.log(bonded.warnings); // 3 plain-English lines naming the risk
// You MUST call fx.trade.fund(...) before bonded.followupDeliverBy
// or the bond is forfeit to the maker.
await pasillo.fx.trade.fund(trade.id, { signature: '0x...', permit2: { /* ... */ } });Read the full hardening spec: docs/plans/2026-05-21-stablefx-bonded-mode-hardening.md.
Error handling
import { PasilloApiError, PasilloNetworkError } from '@bufinance/pasillo-client';
try {
await pasillo.fx.quote({ /* ... */ });
} catch (err) {
if (err instanceof PasilloApiError) {
console.error(err.status, err.code, err.message, err.requestId);
} else if (err instanceof PasilloNetworkError) {
console.error('connection issue', err.cause);
} else {
throw err;
}
}API surface (today)
| Namespace | Method | HTTP |
|---|---|---|
| fx | quote(req) | POST /fx/quote |
| fx | pairs() | GET /fx/pairs |
| fx | balance(currency) | GET /fx/balance |
| fx.trade | create(req) | POST /fx/trade |
| fx.trade | get(id) | GET /fx/trade/:id |
| fx.trade | fund(id, req) | POST /fx/trade/:id/fund |
ramp.*, settle.*, customers.* namespaces land in 0.2.x.
Idempotency
Every POST automatically gets a fresh X-Idempotency-Key (UUIDv4 via crypto.randomUUID). Pasillo caches the response keyed by this UUID for 24h, so a retried network call returns the same trade ID rather than creating a duplicate.
If you need explicit control (e.g. for cross-process retry coordination), open an issue.
Auth
API keys are minted from the b-in-bufi admin panel at /integrations for your Clerk organization. The key is sent as X-API-Key on every request. Scopes are per-key — for FX trade execution your key needs the fx scope.
For Sendero-style internal callers using Vercel OIDC + HMAC signing, use @sendero/pasillo-client instead — different auth path.
Versioning
0.x— pre-1.0, breaking changes possible between minors. Pin a specific version.1.0ships when the Pasillo/fxsurface is feature-complete (P4 admin UI + Pasillo-custodied wallets).
License
MIT.
