Q&A: how would you add Cashtoken support for https://github.com/x402-bch/x402-bch-fac...

Block answers a tech-support question about how would you add Cashtoken support for https://github.com/x402-bch/x402-bch-facilitator/blob/master...

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 only scheme: 'utxo' on BCH mainnet
  • validateUtxo() computes balances in sats and keys the Level DB by txid:vout
  • bch-wallet.js validateUtxo() reads only vout.value (BCH)
  • settlePayment() builds a plain BCH transaction via wallet.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 categoryId in requirements → existing sats logic.
  • Token path: categoryId present → verify utxoValidation.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 cashtoken scheme name, so mirror how EVM handles it: keep scheme: 'utxo' (the proof is still a UTXO) but gate on paymentRequirements.asset/categoryId. If a token is required and the payload’s authorization doesn’t carry categoryId/token amount, reject with invalid_asset.
  • The signed authorization message must include categoryId and tokenQty so the signature binds the token amount (same bchjs.BitcoinCash.verifyMessage flow).

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

  1. 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 settlePayment already “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.

  2. 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 in paymentRequirements.asset so x402 clients and the facilitator share the same field.

Sources

  • Full facilitator source: src/use-cases/facilitator.js and src/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