Q&A: how would you add Cashtoken support for https://github.com/x402-bch/x402-bch-fac...
- The core problem
- 1. Wallet adapter: read token data from the UTXO (bch-wallet.js)
- 2. Make validateUtxo() asset-aware (facilitator.js)
- 3. verifyPayment() — validate scheme and token amount
- 4. settlePayment() — build a CashToken transaction
- 5. listSupportedKinds() — advertise the asset
- Two design decisions worth flagging
- Sources
Question:
how would you add Cashtoken support for https://github.com/x402-bch/x402-bch-facilitator/blob/master/src/use-cases/facilitator.js
Answer:
I’ve reviewed the full facilitator and the wallet adapter. Here’s how I’d add CashToken support.
The core problem
The facilitator is hard-wired to BCH satoshis. Every layer assumes “value = sats”:
listSupportedKinds()advertises onlyscheme: 'utxo'on BCH mainnetvalidateUtxo()computes balances in sats and keys the Level DB bytxid:voutbch-wallet.js validateUtxo()reads onlyvout.value(BCH)settlePayment()builds a plain BCH transaction viawallet.send(outputs)
CashTokens add a second asset dimension (fungible tokens keyed by a category ID). To support them you must make every one of those layers asset-aware. Here’s the plan.
1. Wallet adapter: read token data from the UTXO (bch-wallet.js)
validateUtxo() only extracts vout.value. Minimal-slp-wallet (and @psf/bch-js) expose CashToken data on each vout via vout.tokenData / vout.tokenQty. Extend it to return the token payload when present:
const voutData = txData[0]?.vout?.[vout]
const tokenData = voutData?.tokenData // { categoryId, nft, amount, capability, commitment }
const categoryId = tokenData?.categoryId
const tokenQty = tokenData?.amount // fungible amount, else 0 for NFT-only
return {
isValid: true,
utxoAmountSat: valueSats,
categoryId, // undefined for plain BCH
tokenQty // undefined for plain BCH
}
Also add a validateTokenUtxo({ txid, vout, categoryId }) method that specifically checks voutData.tokenData.categoryId === categoryId and that tokenQty >= required.
2. Make validateUtxo() asset-aware (facilitator.js)
The paymentRequirements object already carries an asset field (the sample comment shows asset: '0x000...0001', the EVM default asset). For CashTokens, the natural mapping is asset = categoryId. Two paths:
- BCH path (unchanged): no
categoryIdin requirements → existing sats logic. - Token path:
categoryIdpresent → verifyutxoValidation.categoryId === categoryId, then track tokenQty instead of sats as the ledger balance.
The Level DB record needs a categoryId field and a token-balance field, and the UTXO id key must be asset-scoped, because one txid:vout can hold BCH and tokens simultaneously:
const assetKey = categoryId ? `${categoryId}:${actualTxid}:${actualVout}` : `${actualTxid}:${actualVout}`
const remainingTokenQty = BigInt(utxoValidation.tokenQty) - callCostToken
Keep remainingBalanceSat for dust/BCH, but add remainingTokenQty and categoryId to the record and to the returned response.
3. verifyPayment() — validate scheme and token amount
Two additions:
- Accept the token scheme. x402 doesn’t define a
cashtokenscheme name, so mirror how EVM handles it: keepscheme: 'utxo'(the proof is still a UTXO) but gate onpaymentRequirements.asset/categoryId. If a token is required and the payload’sauthorizationdoesn’t carrycategoryId/token amount, reject withinvalid_asset. - The signed authorization message must include
categoryIdandtokenQtyso the signature binds the token amount (samebchjs.BitcoinCash.verifyMessageflow).
4. settlePayment() — build a CashToken transaction
This is the hardest part. The facilitator currently does wallet.send(outputs) with sats. For tokens, the facilitator’s wallet must create a token-aware transaction that spends its own token UTXO and pays tokenQty to payTo:
// BCH sats output (dust) + token output to payTo
const bchOutputs = [{ address: payTo, amount: dustSat }]
const tokenOutputs = [{
address: payTo,
amount: tokenQty, // fungible amount
categoryId
}]
// minimal-slp-wallet / @psf/bch-js token send APIs
const txid = await wallet.sendTokenOutputs({ bchOutputs, tokenOutputs })
Note the caveat in the existing code still applies — the facilitator signs and funds the settlement itself, so it must hold tokens in its wallet to cover payouts, just as it must hold BCH today (the existing insufficient_funds check needs a token-balance counterpart: check wallet.getTokenBalance(categoryId)).
5. listSupportedKinds() — advertise the asset
Advertise that token kinds are supported and list accepted category IDs so clients can discover them:
kinds: [
{ x402Version: 2, scheme: 'utxo', network: BCH_MAINNET_CAIP2 },
{ x402Version: 2, scheme: 'utxo', network: BCH_MAINNET_CAIP2,
asset: 'efd2...categoryId...' }
]
Two design decisions worth flagging
-
Facilitator-held inventory. In this design the facilitator pre-funds token settlements from its own wallet. That requires it to acquire token inventory. An alternative (closer to how
settlePaymentalready “just moves funds from facilitator to payTo”) is to treat the payer’s UTXO as the funding source and build the settlement directly from the payer’s signed authorization — but that changes the trust model and requires the payer’s signature to authorize a token spend. -
Category ID is the asset address. Unlike EVM’s
0x...0001, CashToken assets are identified by a 64-hex-char category ID that itself starts with a token type byte (0x01= immutable fungible,0x81= mutable fungible). I’d represent it inpaymentRequirements.assetso x402 clients and the facilitator share the same field.
Sources
- Full facilitator source:
src/use-cases/facilitator.jsandsrc/adapters/bch-wallet.js(x402-bch/x402-bch-facilitator) - x402 token/asset model: docs.x402.org/core-concepts/network-and-token-support (any token on any facilitator via asset identifiers)
- CashTokens spec & fungible amount semantics: cashtokens.org/docs/spec/chip/, PSF wiki
cashtokens.md @psf/bch-js/ minimal-slp-wallet for token-aware reads & sends: npmjs.com/package/@psf/bch-js
Want me to go deeper on any one layer — e.g. the exact token-aware send call in minimal-slp-wallet, or the authorization-signature changes for binding categoryId?
Public Q&A mirror from the Block tech-support bot.
Write a comment