Swap
OUTSIDE BLOQUE
INSIDE BLOQUE
PSEPSE Banks
COP via bank auth
BRBBRE-B Keys
instant payments
SWAP
exchange + rates
→cash-in
←cash-out
├BCOBancolombia
Colombian bank
—→ cash-in: external money enters Bloque (e.g. PSE → Card)
—← cash-out: Bloque balance exits to external destination
The swap module is the boundary between external payment rails and Bloque accounts. Use it to query exchange rates and create cash-in or cash-out orders.
Supported Mediums
Use fromMediums to specify how funds enter the swap, and toMediums to specify where they land.
Overview
The swap module lets you query exchange rates and perform asset swaps between supported mediums and currencies.
Querying Exchange Rates
import { SDK } from '@bloque/sdk';
const bloque = new SDK({
origin: 'your-origin',
auth: {
type: 'apiKey',
apiKey: process.env.BLOQUE_API_KEY!,
},
mode: 'production',
});
const userSession = await bloque.connect('user-alias');
const result = await userSession.swap.findRates({
fromAsset: 'COP/2',
toAsset: 'DUSD/6',
fromMediums: ['bancolombia', 'pse'], // required
toMediums: ['kusama'], // required
amountSrc: '50000000', // 500000.00 COP (scaled by 2 decimals)
sort: 'asc', // optional, default: 'asc'
sortBy: 'rate', // optional, default: 'rate'
});
console.log('Swap rates result:', result);
Parameters
FindRatesParams
Response
FindRatesResult
interface FindRatesResult {
rates: SwapRate[];
}
SwapRate
interface SwapRate {
id: string;
sig: string;
swapSig: string;
maker: string;
edge: [string, string];
fee: Fee;
at: string;
until: string;
fromMediums: string[];
toMediums: string[];
rate: [number, number];
ratio: number;
fromLimits: [string, string];
toLimits: [string, string];
createdAt: string;
updatedAt: string;
}
Fee
interface Fee {
at: number;
value: number;
formula: string;
components: FeeComponent[];
}
FeeComponent
interface FeeComponent {
at: number;
name: string;
type: 'percentage' | 'rate' | 'fixed';
value: number | string;
percentage?: number;
pair?: string;
amount?: number;
}
Complete Example Flow
import { SwapClient } from '@bloque/swap';
const client = new SwapClient({ /* config */ });
async function queryRates() {
const result = await client.findRates({
fromAsset: 'COP/2',
toAsset: 'DUSD/6',
fromMediums: ['bancolombia'],
toMediums: ['kusama'],
amountSrc: '50000000',
});
if (result.rates.length > 0) {
const bestRate = result.rates[0];
console.log('Ratio:', bestRate.ratio);
console.log('Fee:', bestRate.fee.value);
} else {
console.log('No rates available');
}
}
queryRates();
Best Practices
- Validate input parameters before querying rates.
- Use the
fromLimits and toLimits fields to check allowed amounts.
- Handle possible network or API errors gracefully.
- Query rates right before performing a swap to avoid expiration.
- Check the
until field to know how long a rate is valid.
PSE-Specific Best Practices
Required Customer Data
For successful PSE transactions, ensure you provide the following customer data:
bank_code: Valid bank code from pse.banks() list
user_type: 0 for natural persons, 1 for legal entities
customer_email: Valid email address for transaction notifications
user_legal_id_type: Document type ('CC' for citizenship card, 'NIT' for tax ID, 'CE' for foreign ID)
user_legal_id: Valid ID number matching the specified type
customer_data.full_name: Customer's complete legal name
Common Validation Requirements
- Ensure the legal ID number matches the selected ID type
- Use the exact bank code from the banks list (no modifications)
- Provide a valid email format for customer notifications
- Full name should match the customer's legal documentation
Error Handling
try {
const result = await userSession.swap.pse.create({
// ... parameters
});
} catch (error) {
if (error instanceof BloqueValidationError) {
// Handle validation errors (missing fields, invalid IDs, etc.)
console.error('Validation failed:', error.message);
} else if (error instanceof BloqueAPIError) {
// Handle API errors (bank unavailable, rate expired, etc.)
console.error('API error:', error.message);
}
}
Next Steps
Listing PSE Banks
You can retrieve the list of available PSE (Pagos Seguros en Línea) banks and their codes for PSE payments:
const pseBanks = await userSession.swap.pse.banks();
for (const bank of pseBanks.banks) {
console.log(`${bank.code}: ${bank.name}`);
}
Bank Type
interface Bank {
code: string; // Bank code for PSE
name: string; // Bank name
}
This is useful for presenting a list of banks to users when initiating PSE-based swaps or payments.
Create PSE Swap Order
The SDK allows you to create swap orders using PSE (Pagos Seguros en Línea) as the source payment medium. The pse.create method combines order creation and optionally auto-executes the first instruction node to initiate the payment flow.
Basic Usage
// 1. Find available rates
const rates = await userSession.swap.findRates({
fromAsset: 'COP/2',
toAsset: 'DUSD/6',
fromMediums: ['pse'],
toMediums: ['kreivo'],
amountSrc: '1000000', // 10,000.00 COP
});
// 2. Create PSE swap order
const result = await userSession.swap.pse.create({
rateSig: rates.rates[0].sig,
toMedium: 'kreivo',
amountSrc: '1000000',
depositInformation: {
urn: 'did:bloque:account:card:usr-xxx:crd-xxx'
},
args: {
bankCode: '1007',
userType: 0,
customerEmail: 'user@example.com',
userLegalIdType: 'CC',
userLegalId: '123456789',
customerData: {
fullName: 'User Name',
phoneNumber: '3018362958',
}
}, // Auto-executes PSE payment
});
// 3. Redirect user to PSE
if (result.execution?.result.how?.url) {
window.location.href = result.execution.result.how.url;
}
CreatePseOrderParams Parameters
interface DepositInformation {
urn: string; // Account URN where funds will be deposited
// Example: "did:bloque:account:card:usr-xxx:crd-xxx"
}
PsePaymentArgs Type
interface PsePaymentArgs {
bankCode: string; // Bank code (from pse.banks())
userType: 0 | 1; // 0 for natural person, 1 for legal entity
customerEmail: string; // Customer email address
userLegalIdType: 'CC' | 'NIT' | 'CE'; // User legal ID type (e.g., 'CC', 'NIT', 'CE')
userLegalId: string; // User legal ID number
customerData: { // Additional customer data
fullName: string; // Customer's full name
phoneNumber: string;
};
}
CreatePseOrderResult Response
interface CreatePseOrderResult {
order: SwapOrder; // Created order details
execution?: ExecutionResult; // Auto-execution result (if args were provided)
requestId: string; // Request ID for tracking
}
SwapOrder Type
interface SwapOrder {
id: string; // Unique order ID
orderSig: string; // Order signature
rateSig: string; // Rate signature used
swapSig: string; // Swap signature
taker: string; // Taker URN
maker: string; // Maker URN
fromAsset: string; // Source asset
toAsset: string; // Destination asset
fromMedium: string; // Source medium
toMedium: string; // Destination medium
fromAmount: string; // Source amount
toAmount: string; // Destination amount
depositInformation: DepositInformation; // Deposit information
at: number; // Creation timestamp
graphId: string; // Instruction graph ID
status: SwapOrderStatus; // Order status
webhookUrl?: string; // Webhook URL for order events
failureReason?: string; // Failure reason (when status is 'failed')
failureDetails?: string; // Detailed failure information
createdAt: string; // Creation date
updatedAt: string; // Last update date
}
Swap Order States
type SwapOrderStatus =
| 'pending' // Order created, waiting for execution
| 'in_progress' // Order in process of execution
| 'completed' // Order completed successfully
| 'failed' // Order failed
| 'cancelled' // Order cancelled
| 'expired'; // Order expired
ExecutionResult Type
interface ExecutionResult {
nodeId: string; // Executed node ID
result: {
status: ExecutionStatus; // Execution status
args?: unknown[]; // Additional arguments
description?: string; // Result description
how?: ExecutionHow; // Instructions for completing this step
};
}
Execution States
type ExecutionStatus =
| 'pending' // Node pending execution
| 'running' // Node executing
| 'completed' // Node completed successfully
| 'failed' // Node failed during execution
| 'timeout'; // Node timed out
Listing Orders
Retrieve all swap orders for the authenticated user:
const orders = await userSession.swap.listOrders();
orders.forEach(order => {
console.log(`${order.id}: ${order.status} — ${order.fromAmount} ${order.fromAsset} → ${order.toAmount} ${order.toAsset}`);
if (order.failureReason) {
console.log(` Failed: ${order.failureReason}`);
}
});
ListOrdersParams
States and Transitions
Order States
stateDiagram-v2
[*] --> pending
pending --> in_progress
pending --> cancelled
pending --> expired
in_progress --> completed
in_progress --> failed
completed --> [*]
failed --> [*]
cancelled --> [*]
expired --> [*]
Node Execution States
stateDiagram-v2
[*] --> pending
pending --> running
pending --> timeout
running --> completed
running --> failed
running --> timeout
completed --> [*]
failed --> [*]
timeout --> [*]
Complete PSE Flow Example
import { SDK } from '@bloque/sdk';
const bloque = new SDK({
origin: 'your-origin',
auth: {
type: 'apiKey',
apiKey: process.env.BLOQUE_API_KEY!,
},
mode: 'production',
});
async function performPSESwap() {
const userSession = await bloque.connect('user-alias');
// 1. Get list of banks
const { banks } = await userSession.swap.pse.banks();
console.log('Available banks:', banks);
// 2. Find available rates
const rates = await userSession.swap.findRates({
fromAsset: 'COP/2',
toAsset: 'DUSD/6',
fromMediums: ['pse'],
toMediums: ['kreivo'],
amountSrc: '50000000', // 500,000.00 COP
});
if (rates.rates.length === 0) {
console.log('No rates available');
return;
}
// 3. Create PSE swap order with auto-execution
const result = await userSession.swap.pse.create({
rateSig: rates.rates[0].sig,
toMedium: 'kreivo',
amountSrc: '50000000',
depositInformation: {
urn: 'did:bloque:account:card:usr-2p9pn77R6mWHqH96KYKQBylasRD:crd-38uPtyeKTjalMK1jysgoAql3l9n',
},
args: {
bankCode: banks[0].code, // Bank selected by user
userType: 0, // Natural person
customerEmail: 'user@example.com',
userLegalIdType: 'CC',
userLegalId: '123456789',
customerData: {
fullName: 'John Doe',
phoneNumber: '3018362958',
},
},
});
console.log('Order created:', result.order.id);
console.log('Status:', result.order.status);
console.log('Graph ID:', result.order.graphId);
// 4. Redirect user to PSE if redirect URL is available
if (result.execution?.result.how?.url) {
console.log('Redirecting to PSE...');
window.location.href = result.execution.result.how.url;
}
}
performPSESwap();
Create Bancolombia Swap Order
The SDK allows you to create swap orders using Kusama as the source payment medium and Bancolombia as the destination. The bancolombia.create method combines order creation and optionally auto-executes the first instruction node to initiate the swap flow.
Basic Usage
// 1. Find available rates
const rates = await userSession.swap.findRates({
fromAsset: 'COPM/2',
toAsset: 'COP/2',
fromMediums: ['kusama'],
toMediums: ['bancolombia'],
amountSrc: '1000000', // 10,000.00 COP (2 decimals)
});
// 2. Create Bancolombia swap order
const result = await userSession.swap.bancolombia.create({
rateSig: rates.rates[0].sig,
amountSrc: '1000000',
depositInformation: {
bankAccountType: 'savings',
bankAccountNumber: '5740088718',
bankAccountHolderName: 'jon Doe',
bankAccountHolderIdentificationType: 'CC',
bankAccountHolderIdentificationValue: '123456789'
},
args: {
accountUrn: 'did:bloque:card:1231231'
}
});
// 3. Check order status
console.log('Order created:', result.order.id);
console.log('Status:', result.order.status);
CreateBancolombiaOrderParams Parameters
interface BancolombiaDepositInformation {
bankAccountType: 'savings' | 'checking'; // Account type
bankAccountNumber: string; // Account number
bankAccountHolderName: string; // Account holder name
bankAccountHolderIdentificationType: 'CC' | 'CE' | 'NIT' | 'PP'; // ID type
bankAccountHolderIdentificationValue: string; // ID number
}
KusamaAccountArgs Type
interface KusamaAccountArgs {
accountUrn: string; // Kusama account URN for source funds
}
Complete Bancolombia Flow Example
import { SDK } from '@bloque/sdk';
const bloque = new SDK({
origin: 'your-origin',
auth: {
type: 'apiKey',
apiKey: process.env.BLOQUE_API_KEY!,
},
mode: 'production',
});
async function performBancolombiaSwap() {
const userSession = await bloque.connect('user-alias');
// 1. Find available rates for Kusama to Bancolombia
const rates = await userSession.swap.findRates({
fromAsset: 'COPM/2',
toAsset: 'COP/2',
fromMediums: ['kusama'],
toMediums: ['bancolombia'],
amountSrc: '1000000', // 10,000.00 COP
});
if (rates.rates.length === 0) {
console.log('No rates available');
return;
}
// 2. Create Bancolombia swap order with bank details
const result = await userSession.swap.bancolombia.create({
rateSig: rates.rates[0].sig,
amountSrc: '1000000',
type: 'src', // Specify exact KSM amount to pay
depositInformation: {
bankAccountType: 'savings',
bankAccountNumber: '5740088718',
bankAccountHolderName: 'jon Doe',
bankAccountHolderIdentificationType: 'CC',
bankAccountHolderIdentificationValue: '123456789'
},
args: {
accountUrn: 'did:bloque:card:1231231' // Source Kusama account
}
});
console.log('Order created:', result.order.id);
console.log('Status:', result.order.status);
console.log('From Amount:', result.order.fromAmount);
console.log('To Amount:', result.order.toAmount);
console.log('Graph ID:', result.order.graphId);
// 3. Monitor order execution (if applicable)
if (result.execution) {
console.log('Execution started:', result.execution.nodeId);
}
}
performBancolombiaSwap();
Order Types
src (default): User specifies the exact amount to pay. Destination amount is calculated from the rate.
- Example: "I want to pay exactly 10,000 COP, give me whatever DUSD that gets me"
dst: User specifies the exact amount to receive. Source amount is calculated from the rate.
- Example: "I want to receive exactly 5 DUSD, I'll pay whatever COP is needed"
ExecutionHow Type
When a swap order pauses for user action, execution.result.how describes what to do next:
type ExecutionHow = ExecutionHowRedirect | ExecutionHowBrebDeposit;
interface ExecutionHowRedirect {
type: string; // e.g. "REDIRECT"
url: string; // PSE checkout URL
}
interface ExecutionHowBrebDeposit {
type: 'BREB_DEPOSIT';
medium: 'breb';
keyType: string; // e.g. "ALPHA"
keyValue: string; // one-time BRE-B key
amount: string; // scaled COP amount
currency: 'COP';
reference: string;
depositAccountUrn: string;
expectedAmount?: string; // scaled COP required (same as amount on first pause)
receivedAmount?: string; // scaled COP received so far
remainingAmount?: string; // scaled COP still required
depositStatus?: 'awaiting' | 'partial';
}
BRE-B On-Ramp Deposit (COP → Kusama)
Deposit COP via a one-time BRE-B key; the escrow pays out DUSD on Kusama.
const rates = await userSession.swap.findRates({
fromAsset: 'COP/2',
toAsset: 'DUSD/6',
fromMediums: ['breb'],
toMediums: ['kusama'],
amountSrc: '20000000',
});
const result = await userSession.swap.breb.createDeposit({
rateSig: rates.rates[0].sig,
amountSrc: '20000000',
depositInformation: { urn: card.urn },
args: {}, // triggers auto-exec; server fills deposit node args
});
const how = result.execution?.result.how;
if (how?.type === 'BREB_DEPOSIT') {
console.log('Pay via BRE-B:', how.keyType, how.keyValue);
console.log('Required:', how.expectedAmount ?? how.amount, 'COP (scaled)');
}
Errors: E_INVALID_DEPOSIT_INFORMATION when urn is missing.
The graph resumes automatically when the inbound webhook settles (payment.inbound.settled).
Partial payments (underpaid)
If the payer sends less than order.fromAmount, the graph stays paused on the same keyValue and callbackToken. Multiple BRE-B transfers to that key are summed. After each inbound payment, how updates:
// Example: order requires 200,000.00 COP (scaled "20000000"); payer sends 100,000 + 100,000
if (how?.type === 'BREB_DEPOSIT' && how.depositStatus === 'partial') {
console.log(`Received ${how.receivedAmount}, still need ${how.remainingAmount}`);
console.log('Top up to the same key:', how.keyValue);
}
Tracking: the server accumulates received_total_scaled in instruction state. remainingAmount is always derived (expectedAmount − receivedAmount), never stored separately.
Polling: subscribe to swap.order.* webhooks for completion, or poll user.swap.listOrders({ graphId: result.order.graphId }) and inspect order status. The initial createDeposit response does not update when a later partial payment lands — refresh execution/how from the order or webhook payload.
Overpayment
If the payer sends more than required, the swap still delivers the quoted order.toAmount DUSD. Excess COP is flagged to operators for manual refund; it is not converted into extra DUSD.
RTP Payout (Kusama → US Bank)
Cash out DUSD on Kusama to a US bank account via RTP.
const rates = await userSession.swap.findRates({
fromAsset: 'DUSD/6',
toAsset: 'USD/2',
fromMediums: ['kusama'],
toMediums: ['rtp'],
amountSrc: '100000000',
});
const result = await userSession.swap.rtp.create({
rateSig: rates.rates[0].sig,
amountSrc: '100000000',
depositInformation: {
owner: 'Jane Doe',
accountNumber: '1234567890',
routingNumber: '063108680',
accountType: 'checking',
bankName: 'Example Bank',
},
});
Errors: E_INVALID_DEPOSIT_INFORMATION when required bank fields are missing or accountType is invalid.
External US Bank On-Ramp (ACH → Kusama)
Pull USD from a linked US bank account via ACH; funds teleport to Kusama as DUSD.
const rates = await userSession.swap.findRates({
fromAsset: 'USD/2',
toAsset: 'DUSD/6',
fromMediums: ['external-us-bank'],
toMediums: ['kusama'],
amountSrc: '10000',
});
const result = await userSession.swap.externalUsBank.create({
rateSig: rates.rates[0].sig,
amountSrc: '10000',
depositInformation: { ledgerAccountId: 'ledger-user-001' },
args: { sourceAccountUrn: 'did:bloque:account:external-us-bank:abc123' },
});
Errors: E_INVALID_DEPOSIT_INFORMATION when ledgerAccountId is missing.
Next Steps