CryptoChief.Processing
0.9.0
dotnet add package CryptoChief.Processing --version 0.9.0
NuGet\Install-Package CryptoChief.Processing -Version 0.9.0
<PackageReference Include="CryptoChief.Processing" Version="0.9.0" />
<PackageVersion Include="CryptoChief.Processing" Version="0.9.0" />
<PackageReference Include="CryptoChief.Processing" />
paket add CryptoChief.Processing --version 0.9.0
#r "nuget: CryptoChief.Processing, 0.9.0"
#:package CryptoChief.Processing@0.9.0
#addin nuget:?package=CryptoChief.Processing&version=0.9.0
#tool nuget:?package=CryptoChief.Processing&version=0.9.0
Crypto Chief .NET SDK — Crypto Processing API Client
Crypto Chief .NET SDK is the official C#/.NET client library for the Crypto Chief crypto processing API — a unified crypto payment gateway for accepting crypto payments, sending crypto payouts (single and mass), signing on-chain transactions, managing wallets, and verifying webhooks across Ethereum, Tron, TON, Solana, Bitcoin and 20+ more blockchains.
Drop it into any ASP.NET Core, Worker Service, console, or function app to
add cryptocurrency payment processing — stablecoin (USDT / USDC) payouts,
pay-ins, swaps, and smart-contract calls — with typed record requests,
BigInteger amounts, Task<T> async, CancellationToken cooperation, and
first-class IHttpClientFactory integration.
- One-line setup; thread-safe
CryptoChiefClientready for the DI container. - Typed
recordrequest/response DTOs for every documented endpoint. - Contract calls without hand-encoded calldata — Solidity ABI for EVM and TRON, Anchor + Borsh for Solana, Jetton / NFT / comment helpers for TON.
- Local RSA decryption of generated wallet private keys (opt-in).
- Stable error codes via a typed
CryptoChiefApiException, automatic retry on 5xx + transport faults with exponential-with-jitter backoff. - Arbitrary-precision amounts via
System.Numerics.BigInteger— nodouble, ever. - Webhook verification + typed events (
PayoutWebhookEvent,TransactionWebhookEvent,PayInWebhookEvent,StaticDepositWebhookEvent). - Polling helpers:
await client.WaitForPayoutAsync(uuid)blocks until terminal. - Targets .NET 8.0 (LTS) and .NET 6.0 (LTS).
Install
dotnet add package CryptoChief.Processing
Quick start
using CryptoChief.Processing;
using CryptoChief.Processing.Chains;
using CryptoChief.Processing.Models;
var client = new CryptoChiefClient("MERCHANT_ID", "API_KEY");
var estimate = await client.Payouts.EstimateAsync(new EstimatePayoutRequest
{
Network = Chain.EthSepolia,
Coin = "ETH",
Amount = "0.0001",
ToAddress = "0xRecipient...",
});
Console.WriteLine($"recipient receives {estimate.AmountToReceive}");
Both credentials come from the dashboard → Integration tab. The API key is the signing secret — keep it server-side.
Dependency injection (ASP.NET Core / Worker)
using Microsoft.Extensions.DependencyInjection;
using CryptoChief.Processing;
builder.Services.AddCryptoChief(o =>
{
o.MerchantId = builder.Configuration["CryptoChief:MerchantId"]!;
o.ApiKey = builder.Configuration["CryptoChief:ApiKey"]!;
o.LoadRsaPrivateKeyFromFile("rsa_private.pem"); // optional
});
Or bind from IConfiguration:
builder.Services.AddCryptoChief(builder.Configuration.GetSection("CryptoChief"));
The registration uses IHttpClientFactory, so the client respects HTTP
connection pooling and any IHttpClientBuilder policies (Polly, logging,
named handlers) you add downstream.
What you can do with it
| Domain | Service | Key methods |
|---|---|---|
| Single payout (incl. auto-convert swap) | client.Payouts |
EstimateAsync, ExecuteAsync, InfoAsync, HistoryAsync |
| Mass payout (up to 50 items) | client.Payouts |
BatchEstimateAsync, BatchExecuteAsync |
| Two-phase sign / broadcast for arbitrary txs | client.Transactions |
SignAsync, ExecuteAsync, InfoAsync, HistoryAsync |
| EVM / TRON contract calls (incl. ERC-20 / TRC-20) | client.Transactions |
SignEvmCallAsync, SignTronCallAsync, Erc20TransferAsync |
| Solana programs | client.Transactions |
SignAnchorCallAsync, SignSolanaCallAsync |
| TON contract calls (Jetton / NFT / text) | client.Transactions |
JettonTransferAsync, NftTransferAsync, SendTonCommentAsync, SignTonCallAsync |
| Accept incoming payments | client.PayIns |
CreateAsync, SelectAssetAsync, ResetAssetAsync, CancelAsync, InfoAsync, HistoryAsync |
| Wallet management + RSA decrypt | client.Wallets |
GenerateAsync, ListAsync, InfoAsync, HistoryAsync, FreezeAsync, RebindMasterAsync, SetCallbackUrlAsync, SetLabelAsync, DecryptPrivateKey |
| Treasury sweeps | client.Sweeps |
ForceAsync, HistoryAsync, WalletHistoryAsync, SettingsAsync, UpdateSettingsAsync |
| Withdrawals (read-only) | client.Withdrawals |
InfoAsync, HistoryAsync |
| Static-deposit history | client.StaticDeposits |
InfoAsync, HistoryAsync |
| On-chain queries | client.Blockchain |
ContractsAvailableAsync, ContractsListAsync, BlockchainsListAsync, WalletBalanceAsync, TransactionStatusAsync |
| Fiat ↔ crypto rates and catalogues | client.Currencies |
FiatToCryptoAsync, CryptoToFiatAsync, FiatsAsync, CryptosAsync |
| Credits balance & top-up (billing-exempt) | client.Credits |
BalanceAsync, TopupAsync |
Payout with confirmation
using CryptoChief.Processing.Errors;
using CryptoChief.Processing.Polling;
try
{
var payout = await client.Payouts.ExecuteAsync(new ExecutePayoutRequest
{
OrderId = "order-42", // idempotency key — safe to retry
UserId = "u-7",
Network = Chain.EthSepolia,
Coin = "ETH",
Amount = "0.0001",
ToAddress = "0xRecipient...",
UrlCallback = "https://your.app/webhooks/payout",
});
var final = await client.WaitForPayoutAsync(payout.Uuid);
if (final.Succeeded)
Console.WriteLine($"paid: tx={final.TxId}");
}
catch (CryptoChiefApiException ex) when (ex.Code == ErrorCodes.InsufficientFunds)
{
// top up and try again
}
Confirmation fields on PayoutInfo (InfoAsync, ExecuteAsync, HistoryAsync), all optional:
| Field | Type | Meaning |
|---|---|---|
Sources[].Confirmations |
int? |
Confirmations of the source's transaction; absent until it is on chain. |
ServiceOperations[].Confirmations |
int? |
Confirmations of a transaction the platform made for the payout, e.g. a gas top-up. |
Confirmations |
int? |
Lowest count among the sources. |
RequiredConfirmations |
int? |
Confirmations the network requires. |
The payout is PayoutStatus.ConfirmCheck until every source reaches RequiredConfirmations,
then paid. RequiredConfirmations may be absent; on paid with it present,
Confirmations >= RequiredConfirmations.
PayoutWebhookEvent carries the same two payout-level fields; each element of Sources and
ServiceOperations carries confirmations once its transaction is on chain.
WaitForPayoutAsync without options waits 90 minutes (PollOptions.PayoutTimeout). With
PollOptions, every WaitFor* method waits Timeout, 10 minutes by default; for a payout set
Timeout = PollOptions.PayoutTimeout. A TimeoutException means the object is not finished yet;
its last state is in ex.Data["LastSnapshot"].
Two-phase sign + execute
Transactions.SignAsync builds and signs a transaction without
broadcasting. The TTL of the signed reservation varies by chain (EVM 10 m,
UTXO 15 m, TRON 45 s, Solana 60 s, XRP 90 s, TON 300 s) — call ExecuteAsync
before it expires.
using CryptoChief.Processing.Amounts;
using CryptoChief.Processing.Models;
var wei = Amount.HumanToBase("0.0001", 18);
var signed = await client.Transactions.SignAsync(new SignTransactionRequest
{
Network = Chain.EthSepolia,
FromAddress = "0xYourWallet...",
Type = TxType.Native,
ToAddress = "0xRecipient...",
Value = wei.ToString(), // base units (wei)
UrlCallback = "https://your.app/webhooks/transaction",
});
await client.Transactions.ExecuteAsync(new ExecuteTransactionRequest { Uuid = signed.Uuid });
TransactionInfo.Confirmations grows while the transaction is broadcasted. At
RequiredConfirmations the transaction becomes confirmed. Both fields are always sent on
ExecuteAsync, InfoAsync, HistoryAsync and the transaction.* webhook. The webhook is sent
only on a final status; to follow the count, poll InfoAsync. On confirmed,
Confirmations >= RequiredConfirmations.
Contract calls — the easy way
Most real-world transactions are smart-contract calls (token transfers, DEX
swaps, Anchor program instructions, Jetton transfers). You never have to
encode the data field by hand: give the library a typed description, get
back a signed reservation.
EVM — Uniswap V2 swap
This snippet shows the encoder, not a complete swap. Uniswap's router moves your input token with
transferFrom, so it needs an ERC-20approve(address,uint256)on that token first, confirmed before the swap is signed — without it the swap reverts and burns the gas. And anamountOutMinof0accepts whatever the pool returns, which on a public mempool hands the trade to the first sandwich bot that sees it. The runnable version, with both, is inexamples/.
using System.Numerics;
using CryptoChief.Processing.Services;
var amountIn = Amount.HumanToBase("0.01", 18);
var amountOutMin = BigInteger.Zero;
var deadline = new BigInteger(DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds());
var path = new[] { tokenIn, tokenOut };
var signed = await client.Transactions.SignEvmCallAsync(new EvmCallRequest
{
Network = Chain.EthMainnet,
FromAddress = "0xYourWallet...",
Contract = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", // V2 router
Method = "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)",
Args = new object?[] { amountIn, amountOutMin, path, "0xYourWallet...", deadline },
UrlCallback = "https://your.app/webhooks/transaction",
});
The encoder supports uint/int<M>, address, bool, bytes, bytes<N>,
string, and fixed / dynamic arrays of any of those. Argument values accept
BigInteger, plain int/long/uint/ulong, decimal / hex strings,
byte[], and IEnumerable<T> of those. Function-name aliases
(uint → uint256) and parameter names (uint256 amount) are normalised
before hashing.
ERC-20 / TRC-20 transfers have a one-liner:
var amount = Amount.HumanToBase("12.5", 6); // USDT decimals = 6
await client.Transactions.Erc20TransferAsync(new Erc20TransferRequest
{
Network = Chain.EthMainnet,
FromAddress = "0xYourWallet...",
TokenContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
Recipient = "0x...",
Amount = amount,
});
TRON — same encoder, base58 addresses
TRON shares the EVM ABI. SignEvmCallAsync (or its alias SignTronCallAsync)
accepts both base58 (T...) and 0x41-prefixed hex addresses transparently:
await client.Transactions.SignTronCallAsync(new EvmCallRequest
{
Network = Chain.TronMainnet,
FromAddress = "TYourWallet...",
Contract = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", // USDT TRC-20 base58
Method = "transfer(address,uint256)",
Args = new object?[] { "TRecipient...", amount },
});
Need to convert addresses outside a call?
CryptoChief.Processing.Encoders.Tron.TronAddress.ToHex /
TronAddress.FromHex are public.
Solana — Anchor program call
Anchor programs use an 8-byte SHA-256 discriminator (global:<method>)
followed by Borsh-encoded arguments. The SDK builds both:
using CryptoChief.Processing.Encoders.Solana;
using CryptoChief.Processing.Models;
var signed = await client.Transactions.SignAnchorCallAsync(new AnchorCallRequest
{
Network = Chain.SolanaMainnet,
FromAddress = "YourWallet...",
Program = "YourProgramId...",
Method = "initialize",
Args = new[]
{
Borsh.U64(1_000_000),
Borsh.String("hello"),
Borsh.Pubkey("Recipient..."),
},
Accounts = new[]
{
new SolanaAccount { Pubkey = "YourWallet...", IsSigner = true, IsWritable = true },
new SolanaAccount { Pubkey = "DataAcct...", IsSigner = false, IsWritable = true },
new SolanaAccount { Pubkey = "11111111111111111111111111111111", IsSigner = false, IsWritable = false },
},
});
Borsh primitives: Borsh.U8/16/32/64/128, Borsh.I8/16/32/64, Borsh.Bool,
Borsh.String, Borsh.Bytes, Borsh.FixedBytes, Borsh.Pubkey,
Borsh.Option, Borsh.Vec, Borsh.Struct.
Non-Anchor program? Pass pre-built instruction bytes with
SignSolanaCallAsync(new SolanaCallRequest { InstructionData = ..., Accounts = ... }).
TON — Jetton / NFT / comment in one call
TON contract bodies are program-specific cells with no Solidity-style ABI, so the SDK encodes them for you behind high-level helpers. You describe the operation in human terms.
using CryptoChief.Processing.Services;
var amount = Amount.HumanToBase("0.5", 6); // USDT Jetton has 6 decimals
var signed = await client.Transactions.JettonTransferAsync(new JettonTransferRequest
{
Network = Chain.TonMainnet,
FromAddress = "EQYourWallet...",
JettonMaster = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", // USDT
Recipient = "EQRecipient...",
Amount = amount,
Memo = "Order #4242", // optional — wallets show this as the comment
// AttachedTon empty → SDK picks 0.07 TON if the receiver already has a
// Jetton wallet for this token, 0.15 TON if a new one must be deployed.
});
The sender's Jetton wallet address and gas budget are resolved
automatically. If you've already pre-resolved them, pass
JettonWalletAddress and AttachedTon explicitly and no network lookup
happens.
NFT transfer and text comments use the same pattern:
using CryptoChief.Processing.Amounts;
await client.Transactions.NftTransferAsync(new NftTransferRequest
{
Network = Chain.TonMainnet,
FromAddress = "EQYourWallet...",
NftItem = "EQItemAddr...",
NewOwner = "EQRecipient...",
AttachedTon = Amount.NanoTon("0.05"),
});
await client.Transactions.SendTonCommentAsync(new TonCommentRequest
{
Network = Chain.TonMainnet,
FromAddress = "EQYourWallet...",
Recipient = "EQRecipient...",
Text = "Thanks for the coffee!",
AmountTon = Amount.NanoTon("1"),
});
For non-Jetton / non-NFT contracts, build the body cell yourself and pass
the bytes to the lower-level SignTonCallAsync. TonAddress.Parse is
provided for offline address validation / round-tripping the EQ.../UQ...
forms.
Accepting payments (invoices / pay-ins)
A pay-in is an invoice the customer pays in crypto. Use PayInMode.Fiat to
quote the customer in fiat (e.g. $10) and let them pick the coin/network at
payment time, or PayInMode.Crypto to fix the exact coin/amount up front.
using CryptoChief.Processing.Chains;
using CryptoChief.Processing.Models;
using CryptoChief.Processing.Polling;
// FIAT — customer is shown the asset menu
var invoice = await client.PayIns.CreateAsync(new CreatePayInRequest
{
OrderId = $"order-{Guid.NewGuid():N}",
UserId = "user-7",
Mode = PayInMode.Fiat,
AmountFiat = "10.00",
Currency = "USD",
LifetimeSec = 3600,
UrlCallback = "https://your.app/webhooks/invoice",
UrlSuccess = "https://your.app/checkout/success",
UrlError = "https://your.app/checkout/error",
});
// Either send the customer to invoice.PaymentLink (hosted page)
// or implement your own checkout — list invoice.Coins and call SelectAsset:
if (invoice.Status == PayInStatus.WaitingAssetSelect)
{
invoice = await client.PayIns.SelectAssetAsync(new SelectAssetRequest
{
Uuid = invoice.Uuid,
Coin = "USDT",
Network = Chain.TronMainnet,
});
// invoice.ToAddress and invoice.AmountCrypto are now populated.
}
// Block until paid / cancelled / expired.
var final = await client.WaitForPayInAsync(invoice.Uuid,
new PollOptions { Interval = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromMinutes(30) });
Console.WriteLine($"final: {final.Status} ({final.AmountCrypto} {final.PaymentCoin})");
CRYPTO mode fixes the asset up front — no asset-selection step:
var invoice = await client.PayIns.CreateAsync(new CreatePayInRequest
{
OrderId = "order-1",
UserId = "user-7",
Mode = PayInMode.Crypto,
AmountCrypto = "10",
Asset = new Asset { Coin = "USDT", Network = Chain.TronMainnet },
UrlCallback = "https://your.app/webhooks/invoice",
});
// invoice.ToAddress is the deposit address — show it to the customer.
Inbound webhooks land on UrlCallback carrying a PayInWebhookEvent —
verify with WebhookVerifier (see below).
Wallets and RSA-encrypted private keys
When the API generates a wallet it returns the private key encrypted with the RSA public key you uploaded in the dashboard (Project Settings → RSA Key). The SDK can decrypt it locally:
# one-time setup: generate a keypair and upload rsa_public.pem to the dashboard
openssl genrsa -out rsa_private.pem 2048
openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem
var client = new CryptoChiefClient(new CryptoChiefClientOptions
{
MerchantId = "...",
ApiKey = "...",
}.LoadRsaPrivateKeyFromFile("./rsa_private.pem"));
// Or LoadRsaPrivateKeyFromPem("-----BEGIN...");
var w = await client.Wallets.GenerateAsync(new GenerateWalletRequest
{
WalletType = WalletType.Master,
ChainFamily = ChainFamily.Evm,
Label = "Treasury EU", // optional, up to 255 chars, any wallet type
});
// w.PrivateKeyEncrypted is base64 RSA-OAEP / SHA-256 ciphertext.
var privHex = client.Wallets.DecryptPrivateKey(w.PrivateKeyEncrypted!);
// privHex is the chain-native hex form — keep it safe.
LoadRsaPrivateKeyFromPem/File accepts both PKCS#1 (openssl genrsa
default) and PKCS#8 (-----BEGIN PRIVATE KEY-----).
If you skip the option, WalletsService.DecryptPrivateKey throws a
CryptoChiefException and the rest of the SDK continues to work —
decryption is purely opt-in.
Re-pointing a wallet at another master
A transit or static wallet can be moved to another master wallet of the same project after it exists:
var w = await client.Wallets.RebindMasterAsync(depositAddress, newMasterAddress);
Console.WriteLine(w.MasterWalletAddress); // the master it now sweeps to
This moves no money. It changes where the next sweep settles — including sweeps already queued, which will land on the new master — while anything already swept stays on the previous one.
It is idempotent: a wallet already bound to that master answers 200 and changes nothing. Master wallets cannot be re-pointed, and the new master must be of the same chain family and not frozen.
Changing a static wallet's deposit webhook
CallbackUrl can be set at generation time, and rewritten or cleared
afterwards:
await client.Wallets.SetCallbackUrlAsync(staticAddress, "https://your.app/hooks/deposit");
// Clearing it is an empty string, not a null — the SDK sends "" on the wire.
var w = await client.Wallets.SetCallbackUrlAsync(staticAddress, "");
Console.WriteLine(w.CallbackUrl is null); // True
Static wallets only — master and transit wallets are refused with 400. A deposit already announced is not announced again to the new URL.
Naming a wallet
Label can be set at generation time, and renamed or cleared afterwards. It
applies to every wallet type — master, transit and static alike — unlike the
deposit webhook, which is static-only:
await client.Wallets.SetLabelAsync(masterAddress, "Treasury EU");
// Clearing the name is an empty string, not a null — the SDK sends "" on the wire.
var w = await client.Wallets.SetLabelAsync(masterAddress, "");
Console.WriteLine(w.Label is null); // True
Up to 255 characters; longer is refused with LABEL_TOO_LONG.
Label comes back on every response that describes a wallet — generation,
info, the list, and what rebind-master, callback-url and label themselves
return — so a bulk create no longer hands back items you can only tell apart
by address:
var wallets = await client.Wallets.ListAsync();
foreach (var wallet in wallets.Items)
Console.WriteLine($"{wallet.Label ?? "(unnamed)"} — {wallet.Address}");
All three methods return the wallet-info shape, where MasterWalletAddress,
CallbackUrl and Label are always present and null when the wallet has no
such value — never an empty string, never an absent key. A transit wallet
always reads CallbackUrl is null.
Webhooks
Outbound webhooks are signed with the same algorithm used for outgoing requests. The library ships a verifier and typed events:
using CryptoChief.Processing.Webhooks;
using CryptoChief.Processing.Webhooks.Events;
app.MapPost("/webhooks/payout", async (HttpRequest req) =>
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
var sig = req.Headers[WebhookVerifier.SignatureHeader].ToString();
try
{
var evt = WebhookVerifier.VerifyAndDecode<PayoutWebhookEvent>(apiKey, ms.ToArray(), sig);
// process evt...
return Results.Ok();
}
catch
{
return Results.Unauthorized();
}
});
For finer-grained control:
if (!WebhookVerifier.TryVerify(apiKey, body, signature))
return Results.Unauthorized();
WebhookVerifier.SenderIps lists the addresses webhooks are delivered from
— whitelist them at your edge for defence in depth.
Typed event payloads: PayoutWebhookEvent, TransactionWebhookEvent,
PayInWebhookEvent, StaticDepositWebhookEvent.
Error handling
Errors from the API are thrown as CryptoChiefApiException with a stable
Code field:
using CryptoChief.Processing.Errors;
try
{
await client.Payouts.ExecuteAsync(req);
}
catch (CryptoChiefApiException ex)
{
switch (ex.Code)
{
case ErrorCodes.InsufficientFunds: /* need top-up */ break;
case ErrorCodes.AssetNotEnabled: /* unsupported coin/network */ break;
case ErrorCodes.DebtLimitExceeded: /* postpaid debt cap hit */ break;
case ErrorCodes.FromWalletNotOwned: /* wallet doesn't belong to this project */ break;
case ErrorCodes.AlreadyExecuted: /* duplicate execute */ break;
case ErrorCodes.BatchDuplicateOrderId: /* batch validation */ break;
default: /* anything else */ break;
}
}
ex.Code is always a machine code. The platform writes refusals in two
envelope shapes — the code in error when the gateway refused the request
itself, the code in msg when it relayed a refusal from a service behind it
as SERVICE_ERROR — and the SDK resolves both to Code. The English
sentence, where there is one, stays in ex.Message; ex.RawBody keeps the
body as it arrived.
ex.IsRetryable tells you whether the operation is plausibly transient
(5xx, network).
Amounts
Never use double or decimal for crypto amounts. The full base-unit
range exceeds either type's precision. Use Amount.HumanToBase /
Amount.BaseToHuman (backed by System.Numerics.BigInteger):
var wei = Amount.HumanToBase("1.5", 18);
// wei = BigInteger 1500000000000000000
var human = Amount.BaseToHuman(wei, 18);
// human = "1.5"
The API accepts both human strings (the amount field on most endpoints)
and base-unit integer strings (the value field on /transaction/signature).
HumanToBase is precise to the last digit; sub-base-unit precision is
truncated to match every blockchain client's behaviour.
For TON specifically, Amount.NanoTon("0.05") returns the nanoTON decimal
string that AttachedTon / ForwardTonAmount expect.
Configuration
var options = new CryptoChiefClientOptions
{
MerchantId = "...",
ApiKey = "...",
BaseUrl = "https://api-processing.crypto-chief.com", // default
Timeout = TimeSpan.FromSeconds(60),
MaxRetries = 3,
InitialRetryDelay = TimeSpan.FromMilliseconds(200),
MaxRetryDelay = TimeSpan.FromSeconds(5),
UserAgent = "my-service/1.0",
};
options.LoadRsaPrivateKeyFromFile("./rsa_private.pem"); // optional
var client = new CryptoChiefClient(options);
Test mode is a per-project toggle in the dashboard, not a separate base URL — point a test-mode project's credentials at the same client.
Idempotency
Payouts.ExecuteAsync and Payouts.BatchExecuteAsync are idempotent on
OrderId: re-submitting the same order_id returns the same uuid rather
than creating a second payout. The library's automatic retry on 5xx relies
on this — your callers don't need any extra ceremony.
Runnable examples
The examples/ directory has runnable programs you can copy from:
Quickstart— list enabled assets, estimate + execute + poll a payout.InvoiceCreate— accept an incoming crypto payment (FIAT or CRYPTO mode pay-in), select asset, wait for payment.UniswapSwap— V2 swap via one-line ABI encoding.JettonTransfer— TON Jetton transfer with auto-resolved wallet + memo.WebhookServer— ASP.NET Core minimal API that verifies inbound payout / transaction / invoice webhooks.
cd examples/Quickstart
MERCHANT_ID=... API_KEY=... TO_ADDRESS=0x... dotnet run
FAQ — common crypto-processing tasks in C#
How do I accept a crypto payment in .NET?
Create a pay-in (invoice) with client.PayIns.CreateAsync(...); the customer
gets a deposit address and you receive a signed webhook when it's paid. See
PayInsService.
How do I send a crypto payout (withdrawal) in .NET?
client.Payouts.ExecuteAsync(...) with Coin / Network / Amount /
ToAddress. Pass OrderId as an idempotency key and use
client.WaitForPayoutAsync to block until confirmed. Works for native coins
and ERC-20 / TRC-20 stablecoins (USDT, USDC).
How do I send a mass / batch crypto payout?
client.Payouts.BatchExecuteAsync(...) — up to 50 recipients in one signed
request, processed sequentially so the double-spend invariant holds.
How do I call a smart contract (ERC-20, Uniswap) without encoding calldata?
client.Transactions.SignEvmCallAsync(...), or Erc20TransferAsync for a
token-transfer one-liner. Give it a Solidity signature plus args and the SDK
ABI-encodes the data field for you.
How do I transfer USDT on TON (a Jetton) in .NET?
client.Transactions.JettonTransferAsync(...) — pass the Jetton master,
recipient, and amount; the sender's Jetton wallet address and gas budget are
resolved automatically.
How do I move a deposit wallet to a different master wallet?
client.Wallets.RebindMasterAsync(address, newMaster) — it re-points where the
next sweep settles (queued sweeps included) without moving anything already
swept. Transit and static wallets only.
How do I give a wallet a human-readable name?
client.Wallets.SetLabelAsync(address, "Treasury EU"), or Label on
GenerateWalletRequest at creation time. Every wallet type takes one, and
Wallet.Label is returned by every call that describes a wallet — passing ""
clears the name.
A payer says they sent funds and I only have the address — which order was it?
client.Wallets.HistoryAsync(new WalletHistoryQuery { Address = address }) returns the
pay-ins that used that deposit address, as the same PayIn records and Meta block
client.PayIns.HistoryAsync gives you. A deposit wallet serves several orders over its
lifetime, so this is a page of them. The address is matched case-insensitively, and one
your project does not own yields an empty page rather than an error.
Which assets could we turn on, and which are enabled right now?
client.Blockchain.ContractsListAsync() is the platform-wide catalogue — every coin and
token on every network; ContractsAvailableAsync() is what your project can actually be
paid in. Both hand back the same item, so read ChainFamily and IsTest to tell a
testnet asset from a live one, and expect Contract to be "" (not null) on a native
coin.
Which fiat currencies and crypto tickers can I quote against?
client.Currencies.FiatsAsync() lists the fiat codes a pay-in or a rate quote accepts;
client.Currencies.CryptosAsync() lists the crypto tickers the platform has a USDT rate
for, with ByExchange naming which exchange each came from. Rate availability only — a
ticker there is not a promise of deposits, sweeps or payouts in it.
How do I verify a Crypto Chief webhook signature?
WebhookVerifier.Verify(apiKey, body, signature) or
WebhookVerifier.VerifyAndDecode<T>(apiKey, body, signature) for one-line
typed dispatch.
Which blockchains does the crypto processing API support?
Ethereum, BNB Smart Chain, Polygon, Tron, TON, Solana, Bitcoin, Litecoin,
Dogecoin, XRP, Avalanche, Arbitrum, Optimism and more — 25 chains in total.
The constants live in CryptoChief.Processing.Chains.Chain. For the live list — the
chains the platform's scanner is connected to right now — call
client.Blockchain.BlockchainsListAsync().
How do I avoid floating-point rounding bugs with crypto amounts?
Never use double. Convert with Amount.HumanToBase / Amount.BaseToHuman,
which are backed by System.Numerics.BigInteger.
Auto-sweep settings
A deposit wallet is swept to your master wallet on a policy: as soon as funds arrive, once the balance reaches an amount, or never on its own (a force sweep still works).
var s = await client.Sweeps.UpdateSettingsAsync(depositAddress,
typeWork: SweepFieldWrite.Set(SweepPolicyMode.Threshold),
thresholdAmountUsd: SweepFieldWrite.Set("250"));
Console.WriteLine(s.Effective.TypeWork); // what will actually happen
Console.WriteLine(s.Effective.Source); // which layer decided it
SettingsAsync comes back in three layers — Effective (what will happen), Override
(what this wallet decides for itself) and ProjectDefault (what it falls back to) —
because only the three together say whether a value is yours or inherited.
Inheritance is per field: writing the mode leaves the fee mode inherited. A null argument
leaves a field alone; SweepFieldWrite.Inherit stops overriding it — the field is named in
the fields mask with no value, which is the only way to clear one and keep the others.
The mask covers type_work, threshold_amount_usd, fee_mode and gas_source.
Who funds the gas
A deposit wallet holding enough of the chain's native coin pays for its own transfer
whatever the mode is — fee_mode only decides who covers a shortfall.
| Constant | Who covers the shortfall |
|---|---|
SweepFeeMode.Client |
Your own master wallet. |
SweepFeeMode.Service |
The platform — and the cost is billed to your API credits. |
SweepFeeMode.Mix |
The default. client first, falling back to service when the master wallet cannot cover it. |
Who buys the energy on TRON
gas_source says what is bought for a TRON transfer where fee_mode says who covers
the network fees — the two are independent, and the energy is billed to your API credits
under any fee mode.
var s = await client.Sweeps.SettingsAsync(new SweepSettingsQuery { Address = tronAddress });
Console.WriteLine(s.Effective.GasSource); // always concrete — what will actually happen
Console.WriteLine(s.Override?.GasSource); // null = this layer does not decide
Not setting it is not the same as setting native. A wallet that has never chosen a
gas_source gets the platform default, SweepGasSource.Rented — so energy is supplied,
and billed to your credits, without anybody having switched it on. A null in Override
means only that the layer does not decide; the value is inherited, not off. To have the
wallet burn its own TRX, write it explicitly:
await client.Sweeps.UpdateSettingsAsync(tronAddress,
gasSource: SweepFieldWrite.Set(SweepGasSource.Native));
Carried and ignored on every chain other than TRON.
Finding one sweep again
Both history calls take Status (one SweepStatus) and Search (substring). Leaving
Status out includes every status — the SweepStatus.Skipped ones among them, which is
where a sweep that never happened is hiding.
var page = await client.Sweeps.HistoryAsync(new SweepHistoryQuery
{
Status = SweepStatus.Failed,
Search = "0x77EDde", // wallet address, either tx hash, or the task id
});
On WalletHistoryAsync the wallet is already fixed by Address, so Search matches the
transaction hashes and the task id.
A sweep is broadcast first and completed after: while it is SweepStatus.Broadcasted,
SweepConfirmations grows; it becomes SweepStatus.Completed when the count reaches
RequiredConfirmations. The funds have arrived on Completed with SweepConfirmations above
zero. A count above zero without Completed is not settlement. Older records can be Completed
with 0: not settled. The sweep.confirmed webhook is sent once, on completion, with both counts.
CompletedAt is not a settlement signal. It is set when the sweep transaction is sent
(for waiting_gas, failed and skipped, when that status was recorded), so a broadcasted
sweep already has it. Check Status == Completed with SweepConfirmations > 0, or take
ConfirmedAt off the sweep.confirmed webhook.
Manual withdrawals
client.Withdrawals reads withdrawals made from the project's wallets; they are not created
through this API and send no webhooks. Statuses (WithdrawalStatus):
| Status | Meaning |
|---|---|
queue |
Waiting to be processed. |
refueling |
The source wallet is being topped up with native coin for gas. |
refuel_confirmed |
Gas is in place; the transfer is about to be sent. |
sending |
The transfer is being signed and sent. |
broadcasting |
Handed off for broadcast, hash not known yet (EVM). |
in_mempool |
In the mempool, not in a block yet (UTXO networks). |
confirm_check |
Sent; waiting for RequiredConfirmations. |
completed |
Terminal: the transaction reached RequiredConfirmations. |
failed |
Terminal: ErrorReason says why. |
Confirmations is absent before the first block and 0 while no confirmations are counted yet,
including after the transaction left a block (status stays confirm_check). On completed, Confirmations >= RequiredConfirmations.
RequiredConfirmations is always sent.
var wd = await client.Withdrawals.InfoAsync("b0d1f7f9-1eaa-4c2f-8f9b-2b0d1b0b9f11");
if (wd.Succeeded)
Console.WriteLine($"completed at {wd.CompletedAt}: tx={wd.TxHash}, fee ${wd.ActualFeeFiat}");
else if (wd.Status == WithdrawalStatus.ConfirmCheck)
Console.WriteLine($"on its way: {wd.Confirmations?.ToString() ?? "not in a block yet"}/{wd.RequiredConfirmations}");
else if (wd.IsTerminal)
Console.WriteLine($"{wd.Status}: {wd.ErrorReason}");
HistoryAsync returns the same shape per item and pages by Page, PageSize, DateFrom
and DateTo; it has no status filter. Error, ConfirmedAt, UpdatedAt, Contract and
AmountFiat are not sent and stay null; use ErrorReason and CompletedAt.
Documentation
Full guides, tutorials, and recipes → docs-sdk.crypto-chief.com/processing/dotnet
Reference material:
- REST / HTTP API: docs-processing.crypto-chief.com
- NuGet package: nuget.org/packages/CryptoChief.Processing
Contributing
PRs welcome. Please run dotnet test and dotnet build -c Release before
opening; new endpoints should come with a test that exercises the wire shape
through the in-memory HttpMessageHandler fixture in
tests/CryptoChief.Processing.Tests/TransportTests.cs.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net6.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Http (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- System.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Http (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
0.9.0 — Breaking: WaitForPayoutAsync without PollOptions waits 90 minutes (PollOptions.PayoutTimeout); PollOptions.Timeout defaults to 10 minutes. PayoutStatus: Refueling, RefuelConfirmed, Sending, Broadcasting, InMempool, ConfirmCheck; a payout is ConfirmCheck until every source reaches RequiredConfirmations. PayoutInfo: Confirmations, RequiredConfirmations, ServiceOperations (PayoutServiceOperation). PayoutSource: Network, AmountCrypto, NeedRefuel, RefuelAmount, EstimatedFee, EstimatedFeeFiat, FeePaid, FeePaidFiat, TxId, Confirmations; Amount is obsolete, use AmountCrypto. TransactionInfo: Confirmations, RequiredConfirmations; the transaction is Confirmed at RequiredConfirmations. Sweep: RequiredConfirmations; the sweep is Completed when SweepConfirmations reaches it; CompletedAt is the send time, not settlement. Withdrawal: WithdrawalStatus constants, NeedRefuel, RefuelTxHash, RefuelStatus, Confirmations, RequiredConfirmations, ErrorReason, EstimatedFeeFiat, ActualFeeFiat, FeeMode, CompletedAt, IsTerminal, Succeeded; Error, ConfirmedAt, UpdatedAt, Contract and AmountFiat are not sent. PayoutWebhookEvent and TransactionWebhookEvent: Confirmations, RequiredConfirmations. SweepWebhookEvent: RequiredConfirmations.