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).
dotnet add package CryptoChief.Processingusing 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.
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.
| 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 |
EstimateAsync, 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 |
| TRON energy rental (billed to credits) | client.Energy |
QuoteAsync, RentAsync, OrderAsync |
| Native coin purchase (billed to credits) | client.Native |
QuoteAsync, BuyAsync, OrderAsync |
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"].
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.
Transactions.EstimateAsync quotes the network fee without signing or broadcasting and
leaves no record — the same request as SignAsync minus UrlCallback. TxType.Contract is
refused with CONTRACT_ESTIMATE_UNSUPPORTED.
var est = await client.Transactions.EstimateAsync(new EstimateTransactionRequest
{
Network = Chain.EthSepolia,
FromAddress = "0xYourWallet...",
Type = TxType.Native,
ToAddress = "0xRecipient...",
Value = wei.ToString(), // base units (wei)
});EstimateTransactionResponse: EstimatedFee — the network fee in the native coin
(human-readable); Required — the total native coin the from-wallet must hold (fee + value
for a native transfer, fee alone for a token one). EstimatedFeeFiat / RequiredFiat are the
same in USD and are empty strings when the rate is unavailable.
On TRON the response also carries a fee breakdown: FeeExpected (what will actually be
charged given the wallet's current staked / delegated / rented energy pool — not a guarantee,
the pool can run out), FeeLimit (the on-chain cap written into the transaction), Energy
(units needed) and EnergyFee + BandwidthFee + ActivationFee, which add up to
EstimatedFee. ActivationFee is sent only on a native transfer to an address that does not
exist on chain yet. Off TRON all of these are null.
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.
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 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.
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 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.
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).
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.pemvar 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.
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 toThis 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.
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); // TrueStatic wallets only — master and transit wallets are refused with 400. A deposit already announced is not announced again to the new URL.
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); // TrueUp 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 are signed with HMAC-SHA256 v1 using the API key:
| Header | Value |
|---|---|
X-Webhook-Delivery |
delivery id, 1–128 characters [A-Za-z0-9_-]; the same on every attempt and resend |
X-CC-Timestamp |
Unix time of the attempt, seconds, decimal without leading zeros |
X-CC-Signature |
v1= + 64 hex characters |
string_to_sign = "CC-HMAC-SHA256-WEBHOOK-V1" \n X-CC-Timestamp \n X-Webhook-Delivery \n hex(sha256(body))
signature = "v1=" + hex(hmac_sha256(key = apiKey, message = string_to_sign))
Verify the raw request body before parsing it:
using System.Text.Json;
using CryptoChief.Processing.Errors;
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);
try
{
var evt = WebhookVerifier.VerifyAndDecode<PayoutWebhookEvent>(apiKey, ms.ToArray(), req.Headers);
// process evt; deduplicate by req.Headers[WebhookVerifier.DeliveryHeader]
return Results.Ok();
}
catch (WebhookVerificationException)
{
return Results.Unauthorized();
}
catch (Exception ex) when (ex is JsonException or CryptoChiefException)
{
return Results.BadRequest();
}
});VerifyAndDecode<T> throws past a successful verification when the body is not JSON of T
(JsonException) or is JSON null (CryptoChiefException). Neither derives from
WebhookVerificationException, so catch them too: uncaught they answer 5xx, which the platform
retries.
Verify, TryVerify and VerifyAndDecode<T> take the body as ReadOnlySpan<byte> (a string:
Encoding.UTF8.GetBytes(body)) and the headers as any of:
IEnumerable<KeyValuePair<string, StringValues>>— ASP.NET Corereq.Headers;HttpHeaders;IEnumerable<KeyValuePair<string, IEnumerable<string>>>orIEnumerable<KeyValuePair<string, string>>;Func<string, string?>— a value by name, null when absent. One string cannot show every copy of a repeated header: an empty copy is not seen. In ASP.NET Core passreq.Headers, notname => req.Headers[name].
Header names are case-insensitive. Checks, in order:
| Check | Exception |
|---|---|
each header present once; values trimmed of spaces and tabs only; no CR/LF; timestamp decimal without leading zeros, delivery id and v1= + 64 hex in format |
WebhookHeadersException |
| |now − timestamp| ≤ tolerance (default 300 s) | WebhookTimestampException |
| HMAC, constant-time, hex in any case | WebhookSignatureException |
All three derive from WebhookVerificationException (a CryptoChiefException); answer 401.
An empty API key throws ArgumentException. TryVerify returns false instead of throwing.
var ok = WebhookVerifier.TryVerify(apiKey, body, req.Headers, new WebhookVerifyOptions
{
Tolerance = TimeSpan.FromSeconds(60),
Now = () => DateTimeOffset.UtcNow,
});RequestSigner.SignWebhookV1(apiKey, timestamp, deliveryId, body) returns the
X-CC-Signature value; RequestSigner.WebhookV1StringToSign returns the string to sign.
WebhookVerifier.SenderIps lists the addresses the processing platform delivers
webhooks from — whitelist them at your edge for defence in depth. White-label
installations deliver from their own address.
Typed event payloads: PayoutWebhookEvent, TransactionWebhookEvent,
PayInWebhookEvent, StaticDepositWebhookEvent, SweepWebhookEvent.
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 a machine code, read from either error body:
| Body | Code |
Message |
|---|---|---|
{"ok":false,"error":"CODE","msg":"..."} |
error |
msg |
{"ok":false,"error":"SERVICE_ERROR","msg":"CODE"} |
msg |
msg |
{"data":null,"error":{"status":...,"name":"...","message":"...","details":{"code":"CODE"}}} |
error.details.code, else error.name |
error.message |
With no code in the body, Code is HTTP_<status>. ex.Message includes the
message; ex.RawBody keeps the body as received.
ex.IsRetryable tells you whether the operation is plausibly transient
(5xx, network).
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.
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.
Each request is signed with HMAC-SHA256 v1:
| Header | Value |
|---|---|
Merchant |
MerchantId |
X-CC-Timestamp |
Unix time, seconds |
X-CC-Nonce |
32 lowercase hex characters, new on every attempt |
X-CC-Signature |
v1= + 64 lowercase hex characters |
string_to_sign = "CC-HMAC-SHA256-REQ-V1" \n timestamp \n nonce \n METHOD \n path \n query
\n merchant \n idempotency_key \n hex(sha256(body))
signature = hex(hmac_sha256(key = apiKey, message = string_to_sign))
path— route from/v1/, without the base URL and query (/v1/payout/execute).query— without?; empty if none.idempotency_key— empty if not sent.body— the exact bytes sent; empty body hashes toe3b0c442…b855.
The body is the request serialized once with System.Text.Json (snake_case names, null
members omitted); every attempt sends and signs the same bytes.
Timestamp, nonce and signature are computed on every retry. On
SIGNATURE_TIMESTAMP_OUT_OF_RANGE the client sets its clock offset from
server_time (top level, or error.details.server_time) and repeats the
request once.
var signature = RequestSigner.SignHmacV1(apiKey, new HmacV1Input
{
Timestamp = "1789430400",
Nonce = RequestSigner.NewNonce(),
Method = "POST",
Path = "/v1/wallets/info",
Merchant = merchantId,
Body = bodyBytes,
});
// X-CC-Signature: {signature} — the value already carries the v1= prefixPayouts.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.
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 / sweep webhooks.
cd examples/Quickstart
MERCHANT_ID=... API_KEY=... TO_ADDRESS=0x... dotnet runHow 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, req.Headers) over the raw body, or
WebhookVerifier.VerifyAndDecode<T>(apiKey, body, req.Headers) 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.
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 itSettingsAsync 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.
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. |
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 decideNot 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.
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.
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.
A TRON transfer is cheaper when its energy is rented than when TRX is burnt.
client.Energy prices a rental, places it, and reads it back — charges go to the same
project credits balance as everything else. receive_address is the sender of the transfer
the energy pays for.
// Free quote: the rent price next to what burning TRX would cost (Burn*) and the Saving*.
var quote = await client.Energy.QuoteAsync(new EnergyQuoteRequest
{
ReceiveAddress = "TSender...", // who sends the transfer
Energy = 65_000, // optional — platform default when null
});
// Rent is synchronous and requires an Idempotency-Key (400 without it): by the time it
// answers, the energy is either delegated or the refusal reason is known.
var order = await client.WithIdempotencyKey("energy-order-42").Energy.RentAsync(
new EnergyRentRequest { ReceiveAddress = "TSender...", QuoteRef = quote.Ref });
if (order.Status == EnergyOrderStatus.Delivered)
Console.WriteLine($"{order.DeliveredEnergy} energy delegated for {order.Credits} credits");
else
Console.WriteLine($"{order.Status}: {order.Error} ({order.ErrorCode})"); // Credits is null when nothing was charged
// Same order again later, by its idempotency key:
var again = await client.Energy.OrderAsync("energy-order-42");A refused order (HTTP 502, or 402 when the credits balance is short) and an unresolved one
(HTTP 409, NeedsAttention set) come back as the order, not as an exception — the else
branch above is where they land. A retry with the same key returns the same order rather than
renting twice; re-attempt a refusal with a NEW key. A NeedsAttention order must not be
retried — the energy may already be delegated; follow it with OrderAsync. Failures with no
order to report (an error envelope, a gateway error page) throw a CryptoChiefApiException
as usual.
The platform sells native coin (TRX, ETH, BNB, SOL, TON, ...) from its own liquidity, billed
to your project credits balance. The price covers the coins at the market rate plus the fee of
the platform's own transfer — the transfer fee is included, nothing else is charged on top.
total_usd is the full sale price and credits is the exact amount charged to the balance.
receive_address is any address you want the coin on.
// Free quote: the coin cost, the platform's transfer fee and the credits total.
var quote = await client.Native.QuoteAsync(new NativeQuoteRequest
{
Network = "TRON",
ReceiveAddress = "TRecipient...",
Amount = "0.05", // human units
});
// Buy is synchronous and requires an Idempotency-Key (400 without it): by the time it
// answers, the coin is either sent or the refusal reason is known.
var order = await client.WithIdempotencyKey("native-order-42").Native.BuyAsync(
new NativeBuyRequest { QuoteRef = quote.Ref }); // or Network + ReceiveAddress + Amount
if (order.Status == NativeOrderStatus.Delivered)
Console.WriteLine($"{order.Amount} sent as {order.TxHash} for {order.Credits} credits");
else
Console.WriteLine($"{order.Status}: {order.Error} ({order.ErrorCode})"); // Credits is null when nothing was charged
// Same order again later, by its idempotency key:
var again = await client.Native.OrderAsync("native-order-42");A refused order (HTTP 502, or 402 when the credits balance is short) and an unresolved one
(HTTP 409, NeedsAttention set) come back as the order, not as an exception — the else
branch above is where they land. A retry with the same key returns the same order rather than
buying twice; a NEW key re-attempts the purchase. A NeedsAttention order must not be
retried — the coins may already be sent; follow it with OrderAsync. Failures with no order
to report throw a CryptoChiefApiException: a 409 QUOTE_EXPIRED or QUOTE_ALREADY_USED
means quote again (a quote lives about 90 seconds and is single-use), a 402
INSUFFICIENT_CREDITS envelope means the credits balance needs a top-up.
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
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.
MIT — see LICENSE.