TypeScript SDK
@winsznx/blindmarkets-sdk gives you everything you need to build on top of BlindMarkets — intent construction, encryption, gateway communication, and status polling. Works in Node.js and the browser.
Install
npm install @winsznx/blindmarkets-sdkOr with yarn / pnpm / bun — same package name.
Published on GitHub Packages: @winsznx/blindmarkets-sdk
Initialize the client
The GatewayClient handles all HTTP communication. It retries on rate limits and server errors with exponential backoff, and cancels stalled requests after a configurable timeout.
import { GatewayClient } from '@winsznx/blindmarkets-sdk';
const client = new GatewayClient({
baseUrl: 'https://gateway-production-7e9c.up.railway.app',
apiKeyHeader: 'X-API-KEY',
apiKey: process.env.GATEWAY_API_KEY!,
timeoutMs: 10_000,
maxRetries: 3,
retryBaseDelayMs: 200,
retryMaxDelayMs: 5_000,
retryJitterMs: 100,
});Build an intent
IntentBuilder constructs the intent object and computes all the commitments your wallet needs to sign. It validates every field before returning — bad inputs throw a ValidationError, not a silent failure later.
import { IntentBuilder } from '@winsznx/blindmarkets-sdk';
const intent = new IntentBuilder()
.userAddress('0xYOUR_WALLET_ADDRESS')
.assetIn('0xSTRK_CONTRACT_ADDRESS')
.assetOut('0xUSDC_CONTRACT_ADDRESS')
.amount(1_000_000_000_000_000_000n) // 1 STRK (18 decimals)
.minOutput(2_000_000n) // 2 USDC (6 decimals)
.maxFeeBps(50) // 0.5% max solver fee
.deadlineSeconds(300n) // expires in 5 minutes
.privacyMode('HIDDEN_AMOUNT') // hide the amounts
.build();The builder generates a random nonce automatically and derives the intent ID, amount commitment, and intent hash from your inputs. You get back a fully formed Intent object.
Encrypt and submit
Before sending to the gateway, encrypt the intent using the gateway's public key. The encryption uses X25519 key exchange and AES-256-GCM — your actual amounts are never sent in plain text.
import { encryptIntentForGateway, createCommitment } from '@winsznx/blindmarkets-sdk';
// Fetch the gateway's current public key
const { gateway_public_key } = await client.getGatewayPublicKey();
// Encrypt the intent payload
const encrypted = await encryptIntentForGateway(intent, gateway_public_key);
// Build the on-chain commitment (for your wallet to sign)
const commitment = createCommitment(intent);
// Submit to gateway
const response = await client.submitIntent({
intent_id: intent.intentId,
user_address: intent.userAddress,
ciphertext: encrypted.ciphertextHex,
encrypted_session_key: encrypted.encryptedSessionKeyHex,
client_public_key: encrypted.clientPublicKeyHex,
commitment: commitment.intentHash,
user_signature: [], // filled in after wallet signs
nonce: intent.nonce,
});
console.log(response.batch_id); // which batch window this landed in
console.log(response.awaiting_user_transaction); // true — wallet tx still neededSign the commitment with the wallet
After submitting to the gateway, your wallet sends the on-chain commitment transaction. Use starknet.js or the wallet's provider to invoke commit_intent on the IntentRegistry contract:
import { Contract, RpcProvider, WalletAccount } from 'starknet';
const provider = new RpcProvider({ nodeUrl: 'https://starknet-sepolia.drpc.org' });
const account = new WalletAccount(provider, window.starknet);
const registry = new Contract(INTENT_REGISTRY_ABI, INTENT_REGISTRY_ADDRESS, account);
const tx = await registry.commit_intent(
commitment.intentId,
commitment.intentHash,
commitment.amountCommitment,
commitment.minOutputCommitment,
commitment.maxFeeBps,
commitment.deadline,
privacyModeToFelt(commitment.privacyMode),
commitment.nonce,
);
await provider.waitForTransaction(tx.transaction_hash);
// Tell the gateway the wallet tx confirmed
await client.reconcileOnchainIntent(intent.intentId, {
action: 'COMMITTED',
user_address: intent.userAddress,
tx_hash: tx.transaction_hash,
});Why two steps?
Submitting to the gateway and committing on-chain are separate so the encrypted payload reaches the gateway before the on-chain fingerprint is visible. This prevents timing attacks where someone sees the chain tx and races to read the gateway.Poll for status
const status = await client.getIntentStatus(intent.intentId);
// status.status: 'pending' | 'committed' | 'in_batch' | 'settled' | 'failed' | 'cancelled' | 'expired'
// Or list all your intents
const { intents } = await client.listIntents({
userAddress: '0xYOUR_ADDRESS',
status: 'settled',
limit: 20,
offset: 0,
});Error handling
The SDK throws two error types. Import them to catch specifically:
import { ValidationError, NetworkError } from '@winsznx/blindmarkets-sdk';
try {
const intent = new IntentBuilder().build(); // throws — missing required fields
} catch (e) {
if (e instanceof ValidationError) {
console.error('Bad input:', e.message);
}
if (e instanceof NetworkError) {
console.error('Gateway unreachable or returned an error:', e.message);
}
}ValidationError— bad inputs, missing required fields, expired deadlinesNetworkError— gateway returned an error after all retries exhausted
Full type reference
All types are exported from the package root. The key ones:
IntentThe fully built order object returned by IntentBuilder.build()
IntentCommitmentThe on-chain commitment fields returned by createCommitment()
PrivacyMode'PUBLIC' | 'HIDDEN_AMOUNT' | 'HIDDEN_DIRECTION_AND_AMOUNT'
SubmitIntentRequestShape of the POST /v1/intents body
IntentStatusResponseWhat getIntentStatus() returns
BatchListItemOne entry from listBatches()
GatewayClientConfigConstructor config for GatewayClient
Rust SDK
A Rust SDK lives in client-sdk/ in the monorepo. Same concepts — intent construction, gateway client, retry logic. Useful if you're building a solver or a backend service that needs to interact with the gateway.
[dependencies]
blindmarkets-sdk = { git = "https://github.com/winsznx/blindmarkets", subdirectory = "client-sdk" }