Kaspa x402

Kaspa x402 Batch Settlement Binding v2

Status: Alpha.10, Testnet-10-only interoperability candidate

This document defines the active Kaspa network binding for x402 v2 batch-settlement.

Summary

batch-settlement is for repeated or post-priced requests where the buyer provides an off-chain payment commitment at request time and the provider moves value later. The buyer funds one native KAS covenant lane and signs increasing lifetime cumulative ceilings. The provider may settle all or part of the outstanding charge while keeping the lane open.

Use batch-settlement for repeated API requests, metered agent sessions, and variable-cost MCP tools. Use exact for a fixed-price one-shot purchase.

Alpha.10 replaces the earlier alpha batch binding. It does not implement a compatibility reader, migration path, or dual runtime for older alpha channel state. Immutable release snapshots remain historical evidence only.

Scheme And Identifiers

{
  "scheme": "batch-settlement",
  "network": "kaspa:testnet-10",
  "asset": "KAS",
  "extra": {
    "binding": "kaspa-escrow-v2",
    "templateId": "kaspa-x402-escrow-v2"
  }
}

The active identifiers are:

scheme       batch-settlement
binding      kaspa-escrow-v2
templateId   kaspa-x402-escrow-v2

kaspa:testnet-10 is the only Alpha.10 validation target. The common binding reserves kaspa:mainnet as a draft network identifier, but this batch profile MUST NOT be enabled on mainnet.

PaymentRequirements

{
  "scheme": "batch-settlement",
  "network": "kaspa:testnet-10",
  "amount": "1000000",
  "asset": "KAS",
  "payTo": "kaspatest:...",
  "maxTimeoutSeconds": 60,
  "extra": {
    "binding": "kaspa-escrow-v2",
    "templateId": "kaspa-x402-escrow-v2",
    "serverPublicKey": "<32-byte x-only hex>",
    "minDepositSompi": "90000000",
    "claimReserveSompi": "2000000",
    "refundTimeoutDaa": "123456789",
    "claimPolicy": {
      "claimWhenUnclaimedAmountExceeds": "100000000"
    }
  }
}
FieldRequiredRule
schemeyesMUST equal batch-settlement.
networkyesMUST equal kaspa:testnet-10 in Alpha.10.
amountyesMaximum per-request charge, as canonical decimal sompi.
assetyesMUST equal KAS.
payToyesProvider payout address; it is not the lane address.
maxTimeoutSecondsyesPositive response timeout in seconds.
extra.bindingyesMUST equal kaspa-escrow-v2.
extra.templateIdyesMUST equal kaspa-x402-escrow-v2.
extra.serverPublicKeyyesProvider key used by the covenant claim path.
extra.minDepositSompiyesMinimum initial covenant value.
extra.claimReserveSompiyesMinimum successor value retained beyond remaining authorization.
extra.refundTimeoutDaayesAbsolute DAA score for unilateral refund.
extra.claimPolicynoProvider policy for initiating an on-chain claim.
extra.channelStatenoCorrective-only current lane snapshot.
extra.voucherStatenoCorrective-only latest signed voucher proof.

amount is a ceiling. The actual resource charge MAY be lower and is returned as top-level SettlementResponse.amount and SettlementResponse.extensions.kaspa.chargedAmount.

All batch monetary and covenant-state values MUST be in the inclusive range 0..9223372036854775807. SilverScript uses signed 64-bit arithmetic even though the common x402 amount grammar can represent unsigned 64-bit values. Implementations MUST reject a batch value outside the smaller range before constructing a digest, adding values, or evaluating a transaction.

minDepositSompi MUST be at least amount + claimReserveSompi, with the sum also inside the signed 64-bit range.

refundTimeoutDaa is an absolute DAA score and MUST remain below 500000000000, where Kaspa lock time changes to timestamp interpretation.

ChannelConfig And Channel Id

{
  "network": "kaspa:testnet-10",
  "asset": "KAS",
  "templateId": "kaspa-x402-escrow-v2",
  "clientPublicKey": "<32-byte x-only hex>",
  "serverPublicKey": "<32-byte x-only hex>",
  "payTo": "kaspatest:...",
  "refundAddress": "kaspatest:...",
  "refundTimeoutDaa": "123456789",
  "salt": "<32-byte hex>"
}

The application channel id remains:

sha256(
  sha256("kaspa:x402:channel:v1") ||
  sha256(network) ||
  sha256("KAS") ||
  sha256(templateId) ||
  clientPublicKey32 ||
  serverPublicKey32 ||
  sha256(payTo utf8) ||
  sha256(refundAddress utf8) ||
  refundTimeoutDaa_le64 ||
  salt32
)

The channel domain remains v1 because its preimage and meaning did not change. The v2 template identifier already prevents a v1 channel configuration from colliding with a v2 configuration.

Strings are UTF-8 before hashing. Integers are unsigned little-endian values of the stated width. Public keys and salts are decoded from hex to raw bytes. Address decoding MUST verify the network prefix. A serialized script public key is uint16_be version || script bytes; this profile accepts version 0 only.

channelId identifies application configuration. covenantId identifies the on-chain KIP-20 lineage. They are different values and neither one locates the current UTXO.

Domain Tags

kaspa:x402:channel:v1
kaspa:x402:escrow-voucher:v2
kaspa:x402:batch-payment-requirements:v2
kaspa:x402:batch-commitment:v2

Claim, top-up, and refund transaction inputs use Kaspa transaction-v1 SIGHASH_ALL. They do not introduce separate application transaction-signing domains.

Lifetime Accounting

The normative symbols are:

SymbolWire or policy valueMeaning
AchargedCumulativeAmountLifetime actual resource charges durably committed after successful work.
SclaimedCumulativeAmount and covenant stateLifetime gross value settled on-chain, including claim fees.
TsignedMaxClaimable and voucher.amountLatest buyer-signed lifetime cumulative settlement ceiling.
VfundingAmountCurrent value of the active covenant UTXO.
Rextra.claimReserveSompiMinimum value left in the covenant after fully settling the remaining authorization.

Every accepted snapshot MUST satisfy:

0 <= S <= A <= T <= 9223372036854775807
(T - S) + R <= V

Derived values are:

outstanding actual charge = A - S
remaining authorization  = T - S

R is committed by the accepted payment requirements, but it is not a signed entitlement and is not added to T. The provider MUST advertise a conservative value derived from its successor-output policy. The buyer and provider MUST both reject a voucher that leaves less than R beyond its remaining authorization. Claim fees are deducted from the provider payout; live fee estimates affect claim readiness and do not silently change R for an already accepted requirement.

A, S, and T are lifetime values. A claim or top-up changes the current outpoint but does not reset them. The covenant state commits S; application storage commits A, T, the voucher proof, and the current head.

Channel State

{
  "channelId": "<32-byte channel id hex>",
  "covenantId": "<32-byte KIP-20 covenant id hex>",
  "activeOutpoint": {
    "txid": "<current covenant txid>",
    "index": 1
  },
  "activeScriptPublicKey": "<serialized script public key hex>",
  "fundingAmount": "88300000",
  "chargedCumulativeAmount": "2500000",
  "claimedCumulativeAmount": "1700000",
  "signedMaxClaimable": "3000000"
}

The stable covenantId proves identity and authorized successor lineage. It is not a reverse index. Standard node RPC does not provide a covenant-id-to-UTXO lookup, so every runtime MUST persist and atomically advance activeOutpoint, activeScriptPublicKey, fundingAmount, and the covenant state beside the id. Recovery MAY use an indexer, transaction lineage, or a known-address UTXO scan, but it MUST re-verify the same-id transition before adopting a candidate.

PaymentPayload

The supported payload discriminators remain:

Deposit Voucher

deposit-voucher opens a lane or presents an accepted top-up successor and commits to the current request.

{
  "type": "deposit-voucher",
  "channelConfig": {
    "network": "kaspa:testnet-10",
    "asset": "KAS",
    "templateId": "kaspa-x402-escrow-v2",
    "clientPublicKey": "<32-byte x-only hex>",
    "serverPublicKey": "<32-byte x-only hex>",
    "payTo": "kaspatest:...",
    "refundAddress": "kaspatest:...",
    "refundTimeoutDaa": "123456789",
    "salt": "<32-byte hex>"
  },
  "channelId": "<32-byte channel id hex>",
  "escrowAddress": "kaspatest:...",
  "fundingTransaction": "<optional opaque transaction evidence>",
  "fundingOutpoint": {
    "txid": "<current covenant txid>",
    "index": 0
  },
  "fundingAmountSompi": "90000000",
  "activeScriptPublicKey": "<serialized active script public key hex>",
  "voucher": {
    "covenantId": "<32-byte covenant id hex>",
    "amount": "1000000",
    "signature": "<64-byte Schnorr signature hex>"
  }
}

Voucher

{
  "type": "voucher",
  "channelId": "<32-byte channel id hex>",
  "clientPublicKey": "<32-byte x-only hex>",
  "fundingOutpoint": {
    "txid": "<current covenant txid>",
    "index": 1
  },
  "activeScriptPublicKey": "<serialized active script public key hex>",
  "voucher": {
    "covenantId": "<32-byte covenant id hex>",
    "amount": "3000000",
    "signature": "<64-byte Schnorr signature hex>"
  }
}

The outpoint and script remain payload evidence for head synchronization. They are not part of the voucher digest.

Claim

{
  "type": "claim",
  "channelId": "<32-byte channel id hex>",
  "fundingOutpoint": {
    "txid": "<current covenant txid>",
    "index": 1
  },
  "activeScriptPublicKey": "<serialized active script public key hex>",
  "claimAmount": "800000",
  "voucher": {
    "covenantId": "<32-byte covenant id hex>",
    "amount": "3000000",
    "signature": "<64-byte Schnorr signature hex>"
  }
}

Refund

{
  "type": "refund",
  "channelId": "<32-byte channel id hex>",
  "fundingOutpoint": {
    "txid": "<current covenant txid>",
    "index": 1
  },
  "activeScriptPublicKey": "<serialized active script public key hex>",
  "covenantId": "<32-byte covenant id hex>",
  "refundAddress": "kaspatest:...",
  "refundAmount": "87490000",
  "clientSignature": "<65-byte transaction signature hex>"
}

Voucher Digest

The buyer signs a lifetime cumulative ceiling T:

sha256(
  sha256("kaspa:x402:escrow-voucher:v2") ||
  sha256(network utf8) ||
  covenantId32 ||
  T_le64
)

Rules:

Because the digest binds stable covenantId instead of an outpoint, the latest voucher remains valid across every correctly authorized successor. An output with the same script or address but a different id is not the same lane.

Escrow Template

kaspa-x402-escrow-v2 is the byte-exact stateful KIP-20 contract compiled from the normative SilverScript source. The accompanying byte fixture pins the compiler commit, source hash, fixed-width constructor layout, compiled genesis and successor bytes, script public keys, covenant arguments, and voucher digest. Constructor material is derived from the channel config:

clientKey                  = hex_decode(clientPublicKey)
serverKey                  = hex_decode(serverPublicKey)
networkHash                = sha256(network utf8)
payoutScriptPublicKeyHash  = sha256(serialized_script_public_key(payTo))
refundScriptPublicKeyHash  = sha256(serialized_script_public_key(refundAddress))
timeoutLe                  = refundTimeoutDaa_le64
initial state S            = 0

The contract constructor receives timeoutLe as exactly eight bytes and casts it to an integer only inside the refund path. Implementations MUST NOT use a variable-width script-number constructor encoding: changing constructor length would invalidate the compiled successor-script offsets.

An implementation does not need the TypeScript SDK or a SilverScript compiler at runtime. It MAY compile the normative source with the pinned compiler, or it MAY reproduce arbitrary redeem scripts from the fixture's constructorLayout: copy sample.genesis.redeemScript, replace every listed fixed-width slot at every listed byte offset, then derive the version-0 P2SH script public key. Integer slots are signed 64-bit little-endian bytes without an additional push prefix; networkHash is sha256(utf8(network)). The fixture MUST be rejected if its sourceSha256 does not match the published source.

The escrow output script is the version-0 pay-to-script-hash script derived from the complete redeem script. Implementations MUST reconstruct the script and state bytes; a client-provided address, hash, or script is never authority.

S is embedded in the redeem script. An accepted claim changes S, so it MUST derive a new successor redeem script, version-0 P2SH script public key, and corresponding address. Any stored or indexed escrow address MUST advance atomically with activeOutpoint, activeScriptPublicKey, V, and S. A top-up preserves S, so it MUST preserve the redeem script, script public key, and escrow address while advancing the outpoint and V.

Singleton Genesis

The initial accepted funding transaction establishes the KIP-20 lineage. A verifier MUST inspect the full accepted transaction and require exactly one transaction output: the genesis covenant output for the expected template and channel config. That output MUST have a non-zero covenant id, state S = 0, value V at least minDepositSompi, and the exact expected script public key. The funding inputs MUST sum to V + fee; canonical genesis has no change output.

Genesis verification MUST happen before the creating transaction can be pruned. The verifier MUST durably record the transaction id, output index, covenant id, script, value, state, and verification result. A later UTXO carrying an id does not by itself prove that the genesis transaction was singleton or correctly constructed.

The optional fundingTransaction payload field is opaque adapter evidence. The verifier MUST obtain accepted transaction and UTXO data from a trusted Kaspa node or chain adapter.

Canonical Claim

The claim transaction has exactly one input and two outputs:

input[0]  = current covenant head (id C, state S, value V)
output[0] = unbound provider payout, value P
output[1] = sole authorized same-id successor, state S + D, value V - D

The covenant arguments are:

push(provider_transaction_signature_65) ||
push(client_voucher_signature_64) ||
push(T_le64) ||
push(D_le64) ||
claim_selector ||
push(redeem_script)

The provider transaction signature is a 64-byte Schnorr signature plus one-byte SIGHASH_ALL. The voucher signature is the separate raw 64-byte signature defined above.

The covenant MUST enforce:

0 < D <= T - S
S + D <= 9223372036854775807
successor state = S + D
successor value = V - D > 0

It MUST also require exactly one input with id C, exactly one output with id C authorized by that input, the expected successor index, an unbound payout output, the configured payout script hash, and valid state transition.

Application accounting is stricter than the covenant ceiling:

D <= A - S

The provider MAY make a partial claim. T and its signature remain usable for later claims because S advances and the covenant checks only the remaining authorization T - S.

Claim fees come from the provider payout:

successor value V' = V - D
payout value    P  = D - fee
0 < P <= D
transaction fee    = D - P

The lane therefore records S' = S + D; the gross settled amount includes the provider's payout and its claim fee. Fees MUST NOT reduce the successor below V - D or silently consume uncharged buyer value.

Canonical Top-Up

A top-up consumes the current head and creates exactly one authorized same-id successor:

current head  = id C, state S, value V
successor     = id C, state S, value V' where V' > V

The active covenant input MUST be input 0 and uses a client SIGHASH_ALL transaction signature. At least one additional native funding input is required. Output 0 is the sole authorized same-id successor. An optional output 1 MAY return client change only when it is unbound and its script hashes to the configured refund script hash. The transaction MUST contain exactly one input with id C and exactly one authorized output with id C; the change output MUST NOT carry a covenant binding. The successor state MUST equal the input state exactly.

Top-up changes V and the current outpoint. It does not change or reset A, S, T, or the latest voucher proof.

Canonical Refund

The refund transaction terminates the lane after the absolute timeout:

input[0]  = current covenant head (id C, state S, value V)
output[0] = unbound configured client refund, value V - fee

It MUST have exactly one input with id C, no output with id C, one unbound refund output, the configured refund script hash, a valid client SIGHASH_ALL transaction signature, and a lock time satisfying the DAA refund path. lockTimeDaa MUST be greater than or equal to refundTimeoutDaa, and broadcast readiness requires the authoritative current DAA score to be strictly greater than lockTimeDaa. The refund fee MAY reduce the client output. A refund creates no successor and permanently closes that covenant lineage.

Transaction V1 Verification

Claim, top-up, and refund use native transaction version 1. Implementations MUST preserve ordered inputs and outputs, compute budgets, output covenant bindings, lock time, native subnetwork id, gas 0, empty payload, and storage mass in their language-neutral transaction projection.

Before broadcast, a builder or facilitator MUST:

1. load the authoritative current UTXO and persisted head; 2. verify its covenant id, script, value, state, and expected outpoint; 3. construct the exact singleton topology for the selected path; 4. recompute transaction id, hash, sighash, storage mass, script units, and compute budget under active consensus rules; 5. execute the covenant input against the configured consensus implementation.

Adapter-returned identifiers, fees, scripts, states, masses, and finality are evidence to verify, not authority.

Request Processing

The server MUST serialize protected work and state transitions per covenant lane. For a deposit-voucher or voucher request it MUST:

1. validate the x402 envelope, v2 binding, channel config, and lane head; 2. recompute channelId, reconstruct the template, and match the persisted covenantId and current outpoint; 3. verify the latest on-chain S and current V from trusted UTXO data; 4. calculate the required ceiling requiredT = max(previousT, A + PaymentRequirements.amount); 5. require voucher.amount == requiredT, verify the v2 digest and signature, and enforce 0 <= S <= A <= T plus (T - S) + R <= V; 6. durably reserve a work attempt keyed by channel, payment identifier when present, and request fingerprint before invoking the protected handler; 7. execute the protected handler only when that attempt has no staged result, require actualCharge <= PaymentRequirements.amount, and durably stage the result plus actual charge; 8. atomically store the commitment, any payment identifier, `A' = A + actualCharge, T`, and the voucher proof, then mark the attempt applied before releasing the result.

If the handler fails, A and request commitment state MUST remain unchanged. An already accepted genesis or top-up transition remains live and MUST remain recorded. Protected content MUST NOT be released until both application work and the durable payment commit succeed.

If the final payment commit fails after result staging, a retry MUST reuse the staged result and MUST NOT invoke the handler again. There remains an unavoidable application-side-effect window if the process crashes after a non-repeatable handler effect but before staging its result. Such handlers MUST provide their own idempotency or transactional outbox keyed by payment identifier and request fingerprint.

Before a provider claim, the server MUST additionally require 0 < D <= A-S, not merely the covenant-level D <= T-S. After accepted finality it advances the current outpoint, V, and S atomically while preserving A, T, and the voucher signature.

Accepted State Transitions And Crash Safety

Genesis, top-up, claim, and refund become final application state only after the configured accepted-chain finality check. Broadcast-only or mempool-only evidence is pending and MUST NOT release protected content or advance the active head.

Before broadcasting a state transition, the runtime MUST durably save an attempt containing the lane id, expected outpoint, expected state/value, candidate transaction id, operation, and intended successor. Only one open attempt may exist for a lane. After restart, the runtime MUST reconcile that attempt against trusted chain evidence before retrying or reopening the prior head. An uncertain broadcast MUST remain uncertain; it MUST NOT be converted to an unspent assumption.

Applying an accepted attempt MUST use compare-and-set against the exact prior (covenantId, activeOutpoint, S, V) snapshot. A concurrent loser refreshes the current head and MUST NOT execute protected work using stale state.

Commitment Identifier

A stored commitment identifies one successful paid request whose value might not yet have moved on-chain:

sha256(
  sha256("kaspa:x402:batch-commitment:v2") ||
  channelId32 ||
  covenantId32 ||
  requestFingerprint32 ||
  paymentRequirementsHash32 ||
  activeOutpointTxid32 ||
  activeOutpointIndex_le32 ||
  T_le64 ||
  sha256(voucherSignature64) ||
  actualCharge_le64 ||
  chargedCumulativeBefore_le64 ||
  chargedCumulativeAfter_le64 ||
  S_le64
)

chargedCumulativeAfter MUST equal chargedCumulativeBefore + actualCharge and MUST NOT exceed T.

paymentRequirementsHash32 is:

sha256(
  sha256("kaspa:x402:batch-payment-requirements:v2") ||
  sha256("batch-settlement") ||
  sha256(network) ||
  sha256("KAS") ||
  amount_le64 ||
  sha256(payTo utf8) ||
  maxTimeoutSeconds_le64 ||
  sha256("kaspa-escrow-v2") ||
  sha256("kaspa-x402-escrow-v2") ||
  serverPublicKey32 ||
  minDepositSompi_le64 ||
  claimReserveSompi_le64 ||
  refundTimeoutDaa_le64
)

Unknown extra fields are excluded unless a future binding marks them critical and defines their hash contribution.

The durable commitment record MUST include the channel id, covenant id, request fingerprint, requirements hash, current outpoint, voucher and signature, actual charge, A before and after, S, payment identifier when present, and cached response metadata. An idempotent retry with the same payment identifier and fingerprint returns the same commitment and result.

SettlementResponse

Voucher-only success returns the commitment id in transaction because the request has not necessarily caused an on-chain transaction:

{
  "success": true,
  "transaction": "<commitment id hex>",
  "network": "kaspa:testnet-10",
  "payer": "kaspatest:...",
  "amount": "700000",
  "extensions": {
    "kaspa": {
      "commitmentId": "<commitment id hex>",
      "covenantId": "<32-byte covenant id hex>",
      "chargedAmount": "700000",
      "channelState": {
        "channelId": "<32-byte channel id hex>",
        "covenantId": "<32-byte covenant id hex>",
        "activeOutpoint": {
          "txid": "<current covenant txid>",
          "index": 1
        },
        "activeScriptPublicKey": "<serialized script public key hex>",
        "fundingAmount": "88300000",
        "chargedCumulativeAmount": "2500000",
        "claimedCumulativeAmount": "1700000",
        "signedMaxClaimable": "3000000"
      }
    }
  }
}

For deposit-voucher, top-level amount remains the resource charge and extensions.kaspa.fundingAmount reports the escrow funding separately.

An accepted claim returns its on-chain transaction id and the updated current outpoint. The returned signedMaxClaimable remains T; it MUST NOT reset to zero. An accepted refund returns its transaction id and terminal lane status.

Corrective 402 And Recovery

A corrective PaymentRequired response SHOULD include extra.channelState. When it asks the buyer to adopt a higher lifetime ceiling, it MUST also include extra.voucherState containing covenantId, amount, and signature. The client MUST verify that proof against the trusted network and lane id before adopting it.

Corrective responses cover stale current outpoints, stale A/S/T, missing local sessions, channel locks, insufficient V, and restart recovery. A client MUST NOT infer the current outpoint from covenantId alone.

If a server loses an unclaimed voucher, it may recover from the covenant state and a client-provided signed voucher. It cannot reconstruct application charges or signatures from the chain. Unrecoverable off-chain charges are provider risk.

Trust Boundary

The covenant cryptographically enforces the lifetime ceiling T, stable same-id lineage, payout destination, successor state/value, top-up state preservation, and refund destination/timeout. It does not prove which resource work occurred or the actual charge A.

The provider MUST enforce D <= A-S. A malicious provider that settles more than actual outstanding charges but no more than T-S violates application accounting even though the covenant may accept the transaction. Buyers should stop signing and refund if returned cumulative charges, current head, covenant id, or voucher proof cannot be reconciled.

Security Requirements

Implementations MUST reject:

Interoperability Evidence

Alpha.10 vectors MUST cover channel id, v2 voucher digest, structured requirements hash, request commitment, singleton genesis, partial claim and same-voucher reuse, top-up, refund, signed-int64 boundaries, reserve failures, concurrent attempts, and transaction-v1 full-consensus execution.

The specification, normative SilverScript source, and language-neutral byte fixture form the portable source of truth. The TypeScript builders and independent Rust consensus harness are tested implementations and review evidence; neither is required to implement this profile. Testnet-10 evidence is alpha validation only; it is not a mainnet readiness claim.

Local Diagnostics

Public responses use errors.md. Implementations MAY retain local invalid_kaspa_batch_* diagnostics for template, channel, covenant, genesis, head, voucher, accounting, balance, busy, commitment, claim, top-up, refund, compute-budget, and crash-recovery failures, but MUST map them to the public x402 error vocabulary on the wire.

Source: /spec/kaspa-batch-settlement-v2.md