Kaspa x402

1.0.0-rc.2 released · Testnet-10 · Release details

Kaspa x402 Hash-Chain Exact Binding v1

Status: published in 1.0.0-rc.2 as an optional Testnet-10 profile. It is included in the public packages and hosted browser demo. The binding remains an interoperability candidate; mainnet and stable-release gates still apply. See the RC2 release reference.

This document specifies a native-KAS x402 v2 exact mechanism derived from KCC20's hash-chain/v1 borrowed-receive authorization, using OTP-style one-time spending grants. It is a separate, optional binding. It does not change the active kaspa-exact-v2 standard-native or additive profiles, and it is not a KCC20 token transfer.

Normative sources for this binding: KCC20 borrowed-receive design, KCC20 §5, KIP-20 covenant IDs, x402 v2, and x402 exact.

OTP-style one-time authorization

The OTP-style feature is the one-time borrowing grant: a hash-chain link and its corresponding confidential Schnorr signing key. It lets a payer make a native-KAS payment without the merchant co-signing that transaction.

  1. The merchant privately assigns the current link and key to one payer's request through the grant claim.
  2. The payer uses that key to sign the head spend, adds its own funding, and independently broadcasts the payment.
  3. Once the spend is accepted, the successor head has a new hash-chain guard. The consumed grant cannot authorize another spend against that successor.

Here, “OTP-style” means one accepted head transition, rather than a login code. The key can sign competing transaction attempts before one is accepted. Grant expiry is off-chain policy; an unused disclosed grant remains usable until accepted head advancement or owner rotation invalidates it. Reorgs require reconciliation before further grants are issued.

Purpose and trust model

The merchant keeps a native-KAS covenant head. It precomputes a finite hash chain and gives one current predecessor guard plus its one-time Schnorr private key to one payer. The payer can spend the head and fund its positive KAS increase without a merchant signature. The covenant accepts one successful transition for that released link, then changes its guard. This is Michael Sutton's suggested one-use borrow authorization applied to native KAS.

The link is a bearer capability for one accepted head transition. It does not by itself bind a price, payer, request or wall-clock expiry. A holder may sign several conflicting transactions before one is accepted. The x402 layer requires an exact price and request-bound payer signature before providing a resource. A consensus-valid smaller top-up may still consume the grant and move the head without buying the resource. That outcome must be reconciled, not described as a failed on-chain payment.

Identifiers and payment flow

Field Value in this binding
scheme exact
network kaspa:testnet-10 for validation; kaspa:mainnet is reserved
asset KAS (sompi, 8 decimals)
extra.binding kaspa-hash-chain-exact-v1
extra.profile hash-chain-additive
extra.templateId kaspa-x402-hash-chain-head-v1
extra.assetTransferMethod kaspa-v1-hash-chain-proof
extra.paymentFlow upfront

This binding has one asset transfer method. It belongs to x402 exact's client-submitted payment-proof family: the payer signs and broadcasts the native-KAS transaction, then presents the complete transaction as proof. The server's /settle checks trusted chain evidence, atomically claims the proof for the request, and only then invokes protected work. /verify may exist for diagnostics but is not in the upfront resource flow and must not consume the payment. The payer funds all network fees. No facilitator or merchant key is required to sign the borrow spend. Clients that do not understand this method or upfront must skip the offer.

The canonical network payment identifier is kaspa:testnet-10:<lowercase canonical transaction id> (or the matching network identifier on a future enabled network). Atomic proof claiming is scoped across every process that can serve the same merchant head. A transaction's double-spend protection does not prevent delivery of multiple resources for one payment.

Head and hash-chain rule

The merchant creates random x_0 and one-time Schnorr keypairs (private_key_i, pubkey_i) and computes x_i = BLAKE3_unkeyed_32(x_(i-1) || pubkey_i). The initial head guard is x_n. For a current guard x_i, a borrower supplies revealedGuard = x_(i-1), the 32-byte x-only pubkey_i, and a 65-byte Schnorr transaction signature made with private_key_i. The borrow branch MUST check the exact 64-byte concatenation hash, the transaction signature with SIGHASH_ALL, and a positive native-KAS increase. Its successor state guard MUST equal revealedGuard. Links are released in reverse generation order.

The initial head is a version-1 KIP-20 covenant output with a stable covenantId. Each borrow spends exactly the advertised current head at input 0 and creates exactly one same-ID authorized successor at output 0. The successor script MUST be the same pinned contract template and owner with the new guard, independently reconstructed and compared with output 0. The head principal MUST remain in that successor. KIP-20 identity does not locate the live outpoint; the merchant must persist and reconcile it.

The pinned SilverScript contract has two separate owner paths, both covered by the local consensus vector:

An owner rotation and a released borrow are competing spends of the same outpoint. Local expiry or key deletion does not revoke a grant. Revocation is effective only when an owner transition is accepted ahead of the borrower.

PaymentRequirements

This illustrative offer uses placeholders, not a conformance vector:

{
  "scheme": "exact",
  "network": "kaspa:testnet-10",
  "amount": "20000000",
  "asset": "KAS",
  "payTo": "kaspatest:<expected-successor-p2sh-address>",
  "maxTimeoutSeconds": 300,
  "extra": {
    "binding": "kaspa-hash-chain-exact-v1",
    "profile": "hash-chain-additive",
    "assetTransferMethod": "kaspa-v1-hash-chain-proof",
    "paymentFlow": "upfront",
    "templateId": "kaspa-x402-hash-chain-head-v1",
    "finality": "accepted",
    "transactionEncoding": "kaspa-sdk-safe-json-v2.0.0",
    "payToScriptPublicKey": "<successor serialized script public key>",
    "headId": "<32-byte server-scoped id>",
    "headVersion": "7",
    "covenantId": "<32-byte KIP-20 id>",
    "expectedHeadOutpoint": { "txid": "<current tx id>", "index": 0 },
    "headAmount": "100000000",
    "headScriptPublicKey": "<current serialized script public key>",
    "headRedeemScript": "<current pinned redeem script>",
    "currentGuard": "<32-byte current guard>",
    "nextGuard": "<32-byte predecessor guard>",
    "oneTimePublicKey": "<32-byte x-only public key>",
    "grantId": "<32-byte grant id>",
    "grantClaimUrl": "https://api.example.com/.well-known/kaspa-x402/grants",
    "challengeId": "<32-byte request challenge id>",
    "challengeIssuedAt": "2026-09-22T12:00:00.000Z",
    "challengeExpiresAt": "2026-09-22T12:05:00.000Z"
  }
}

amount MUST be a canonical positive uint64 sompi string, and headAmount + amount MUST fit in uint64. finality MUST be accepted or confirmed; mempool presence is insufficient to deliver the resource. The challenge MUST bind one normalized resource request at issuance, and challengeExpiresAt MUST be after challengeIssuedAt and no later than challengeIssuedAt + maxTimeoutSeconds. The issuer MUST persist both times and the request hash with the offered requirements. For this profile, requestHash is SHA-256 of canonical {method, url, body} after binding the host-trusted security context. It excludes PaymentRequirements: those requirements contain the challenge that cannot exist until the request hash has been calculated. The separate payer authorization binds the complete PaymentRequirements hash. Challenge issuance MUST require a host-authenticated admission context before it consumes durable issuer state. The issuer MUST deduplicate an identical live (admission key, requestHash, amount, head version, grant) offer without extending its expiry, enforce a durable per-admission-key live challenge cap, and retain the global head cap. If authenticated admission is unavailable or a cap is reached, a server offering batch settlement SHOULD return that usable fallback without allocating a hash-chain challenge. headId is the merchant's stable application identifier, while covenantId is the independently verified KIP-20 identity. headVersion increases for every accepted borrow or owner rotation. The outpoint, amount, script, redeem script, guard and covenant ID MUST agree with trusted current-head evidence when the offer is made. At settlement, the verifier MUST authenticate that historical predecessor and the accepted successor; the offered outpoint will no longer be unspent after a successful payment.

The published template ID MUST resolve to one pinned SilverScript source, compiler commit, deployable ABI artifact, and template hash. A server MUST derive currentGuard, owner, and template from headRedeemScript and reject any disagreement. It MUST verify BLAKE3_unkeyed_32(nextGuard || oneTimePublicKey) == currentGuard before advertising. It MUST derive the expected successor redeem script by changing only the guard, then derive payToScriptPublicKey and payTo for the selected network. The current head address and successor payTo are normally different.

payTo is the expected successor P2SH address. This binding identifies the exact merchant transfer as output[0].value - trusted_previous_head.value == amount, with the old merchant principal carried across the same owner, template and covenant ID. There is no second merchant payment output. This net-gain interpretation of x402 exact is explicit here and requires interoperability review against the upstream requirement for one identifiable transfer of amount to payTo before this profile is advertised beyond its own implementation.

nextGuard and oneTimePublicKey are public commitment data and do not authorize a spend without the private key. The private key MUST NOT appear in PaymentRequired, PaymentPayload, public headers, logs, URLs, vectors or settlement responses. grantClaimUrl MUST be same-origin HTTPS with the client's actual protected request URL, not merely an advertised resource.url, except for a loopback development server when the actual request is loopback. Before signing or posting a claim, an automated client MUST require that exact canonical origin in a dedicated outbound allowlist. This allowlist is separate from funding policy and explicitly trusts all public or private addresses to which the origin can resolve; a client that cannot pin DNS and transport to a prevalidated peer MUST NOT represent a pre-resolution check as rebinding protection. Redirects, credentials and fragments are forbidden. Plain HTTP is limited to an explicitly listed loopback origin. grantId MUST be a fresh unpredictable 32-byte value for the current head; challengeId MUST be fresh for its request. A server may offer multiple public challenges for one current head, but only one payer may claim its live grant. Other claimants must receive an unavailable result and obtain a fresh offer after the head changes or owner rotation completes.

Private grant claim

The client claims the advertised grant through POST grantClaimUrl before building its transaction. The claim body contains the selected grantId, challengeId, the normalized requestHash, a 32-byte x-only payerPublicKey, a UTC expiresAt, and a 64-byte Schnorr signature. The signature signs SHA-256 of the UTF-8 bytes of the following object after the same recursive canonical JSON serialization used by kaspa-exact-v2:

{
  "scope": "kaspa-x402-hash-chain-grant-claim-v1",
  "network": "kaspa:testnet-10",
  "binding": "kaspa-hash-chain-exact-v1",
  "grantId": "<lowercase id>",
  "challengeId": "<lowercase id>",
  "requestHash": "<lowercase hash>",
  "payerPublicKey": "<lowercase x-only key>",
  "expiresAt": "2026-09-22T12:05:00.000Z"
}

The resource server MUST independently compute requestHash, compare every claim field to its still-live, stored offer and request, verify the payer signature, and reject a claim expiry later than challengeExpiresAt or the server's policy window. Before reading or parsing a claim body, the claim endpoint MUST acquire a bounded global or host-authenticated request permit. After parsing only the bounded claim fields, it MUST charge the same stored authenticated admission bucket used for its challenge before signature, eligibility, database mutation or chain work. It MUST enforce request-body, concurrency and timeout limits and propagate caller cancellation through current-head observation. The server MUST first authorize the claim without assigning the key, read the exact advertised head outpoint from an authoritative complete and untruncated current-UTXO source, and only then atomically revalidate the head version, outpoint, amount, script, covenant and claim tuple while committing the assignment. A transient observer failure, timeout or cancellation MUST leave the grant unassigned. The issuer MUST NOT expose an unchecked assignment helper: every public claim operation must require the authoritative exact-outpoint observation before its atomic commit. The grant issuer MUST atomically assign the current grant to one (payerPublicKey, requestHash, challengeId) tuple and persist the assignment before delivering the key. An identical authenticated retry receives the same assignment; a different tuple cannot receive that key. A crash after assignment must not make the key silently available to a second payer. A signature proves control of payerPublicKey, not entitlement to a scarce grant: the issuer MUST enforce its configured client eligibility and rate limits before assignment.

The confidential response returns grantId, nextGuard, oneTimePublicKey, the matching one-time private key, and the effective claim expiry. The client MUST verify the private key corresponds to oneTimePublicKey and the public guard equation before using it. The response MUST set Cache-Control: no-store; the server and intermediaries MUST NOT log or cache its body. Keys at rest need the same confidentiality and recovery policy as other transaction-signing material. The grant may be delivered through an equivalent authenticated confidential channel for a non-HTTP x402 transport, but its fields and allocation rules remain the same.

Claim expiry is off-chain service policy. An issued key remains a valid on-chain borrower authorization until the head advances or an owner rotation wins. On abandonment, the issuer stops offering the old head, attempts owner rotation, and waits for trusted accepted-chain evidence before issuing a new grant. On a small but valid unsolicited top-up, the issuer observes and adopts the authentic successor; it does not serve a resource for an unmatched or underpaid transaction. A reorg that restores a previously issued guard puts the head into reconciliation hold; the issuer must not release another grant until the actual chain state and key exposure are resolved.

The reference issuer persists the quoted amount with each delivered grant. If head readback puts an assigned grant on hold after an unmatched borrow, an operator supplies that transaction ID to recoverSelectedUnmatchedBorrow with a trusted selected-chain view. The issuer checks the spent outpoint, same-ID successor, revealed guard and positive increase against the durable quote before advancing. A spend with the exact quoted increase stays on the normal x402 settlement path; it cannot be reclassified as an unmatched borrow to bypass payment delivery. Quote-free assignments from older local stores remain unavailable for this automatic recovery and require investigation.

Canonical payment transaction

The payer signs and broadcasts a version-1 native Kaspa transaction:

input[0]  = exact current merchant hash-chain head
input[1+] = standard Schnorr P2PK payer funding inputs

output[0] = same-ID successor head at payTo,
            amount = headAmount + PaymentRequirements.amount,
            state.guard = nextGuard
output[1] = optional payer-controlled change

The complete signed transaction MUST spend expectedHeadOutpoint at input 0, use the borrow entrypoint with the advertised nextGuard and oneTimePublicKey, verify the one-time SIGHASH_ALL signature, and produce exactly one KIP-20 same-ID authorized output at index 0. It MUST match the trusted current head value, script, covenant ID and pinned template. Output 0 MUST match the independently reconstructed successor script, covenant binding, payToScriptPublicKey, and headAmount + amount. It MUST preserve the owner and all non-guard template fields. The covenant itself enforces a positive increase; the x402 verifier enforces this exact equality.

There MUST be at least one payer input after input 0, at most one payer change output, no separate merchant output, and no extra covenant input or output. Each payer input MUST be a verified standard Schnorr P2PK input; optional change MUST return to a script controlled by a verified payer input. The network fee comes entirely from payer inputs: sum(payer inputs) = amount + fee + payer change. The transaction MUST use the native subnetwork, zero gas and lock time, empty payload, final signed witnesses, sufficient v1 compute budgets, correct contextual storage mass, and configured amount, fee, size and script-unit bounds. Validation MUST include current Rusty-Kaspa isolation, populated-transaction, mass and script rules; supplied UTXO hints are not authority.

The payer may broadcast through its chosen Kaspa node. That independent broadcast ability is part of the delegated-grant design. A signed transaction is not an x402 receipt until the server observes it at the required finality, validates its exact terms, and atomically claims it for one request.

PaymentPayload and request binding

The paid request carries a full signed transaction as proof:

{
  "x402Version": 2,
  "accepted": { "...": "the complete selected requirements above" },
  "payload": {
    "type": "exact-transaction",
    "profile": "hash-chain-additive",
    "transaction": "<complete signed safe transaction JSON>",
    "transactionEncoding": "kaspa-sdk-safe-json-v2.0.0",
    "paymentOutputIndex": 0,
    "grantId": "<32-byte grant id>",
    "challengeId": "<32-byte challenge id>",
    "requestHash": "<normalized request hash>",
    "authorization": {
      "version": "kaspa-x402-exact-request-authorization-v1",
      "digest": "<32-byte digest>",
      "inputIndex": 1,
      "expiresAt": "2026-09-22T12:05:00.000Z",
      "signature": "<64-byte payer Schnorr signature>"
    }
  }
}

accepted MUST be canonically identical to the selected server-issued requirements; the server MUST reject altered or unissued offers. It independently computes requestHash; it MUST NOT trust only payload.requestHash. The payer's separate request-authorization signature uses the existing kaspa-x402-exact-request-authorization-v1 digest with profile = "hash-chain-additive", output index 0, this binding's successor payTo, the complete requirements hash, the grant's challengeId, the recomputed transaction ID, payer funding input index, and expiry. The requirements hash includes the grant ID, head and next-guard terms. The signature's public key MUST equal the key that claimed the grant and be proven by its authoritative P2PK funding input at an index of at least 1. The one-time borrow key cannot serve as payer request authorization.

The transaction ID MUST be recomputed from the canonical transaction; client-supplied IDs, UTXO values, finality or fee claims are not authority. authorization.expiresAt and challengeExpiresAt MUST be live for a new proof claim, and the former MUST be no later than the latter and the maxTimeoutSeconds window. A previously accepted immutable attempt may be recovered after expiry only under the existing exact replay rules; expiry never authorizes a new handler run. A client MUST recheck both expiries immediately before every broadcast and MUST NOT broadcast an expired attempt; it may only reconcile that exact transaction through trusted chain evidence. Clients should allow time for network acceptance before expiry. A payment that becomes final only after its presentation window closes has no automatic resource entitlement or refund under this binding; that consequence must be exposed before wallet approval.

Settlement, replay, and finality

  1. The merchant advertises the current public head/link terms. A payer authenticates and claims the one live private grant.
  2. The payer builds, signs and broadcasts the complete transaction, then presents the transaction artifact and request authorization in x402.
  3. /settle checks size limits, schema, signed request identity, grant assignment, current or durably observed head lineage, trusted transaction and historical UTXO data, exact net increase, and requested finality. It independently recomputes the transaction ID and validates every signature, witness, covenant binding, fee and mass under current consensus. The server itself, before invoking any custom verifier adapter, MUST locally authenticate and durably bind the first transaction ID to the assigned head version, grant, challenge, request and payer. An alternate candidate MUST fail before external reads. A matching candidate already recorded as the accepted payment for that durable delivery MUST remain an idempotent retry after issuer head advancement. Input count MUST be bounded. Historical origins SHOULD be fetched in one cancellable batch, and accepting-block reads MAY be deduplicated only within that verification; all node reads MUST share the caller's deadline.
  4. If finality is not yet met but can still be reached, settlement reports the unmet condition and releases any transient proof claim. It does not run protected work. A retry may present the same immutable proof.
  5. Once finality is met, an atomic store consumes the canonical network transaction ID, grant ID, expected head/version, challenge and payment-identifier for the one normalized request. The accepted head successor is recorded durably before protected work.
  6. The handler runs once. Its result and the payment/response commit are durable so an identical retry resumes the result. A different request presenting the same transaction or grant fails.

Clients MUST canonicalize the actual request URL before deriving new payment identifiers and intent records. Recovery MAY recognize equivalent historical spellings, including an explicit default port, only when the canonical method, URL, body, origin and request hash still match. If two aliases identify different attempts, recovery MUST fail closed.

The proof is presentable only while its claim and request-authorization window is live, apart from recovery of an already accepted identical attempt. The maximum age for a new proof claim is the advertised interval from challengeIssuedAt to challengeExpiresAt, capped by maxTimeoutSeconds; no old transaction can be attached to a new challenge. Replay records therefore must be retained at least through that window, any unresolved reorg/broadcast state, and the server's durable idempotency period. The head outpoint is itself single-use on-chain, but that does not replace cross-process x402 delivery deduplication. If an already observed transaction spent the advertised head with too small or too large an increase, it is never a successful payment for this quote; the merchant must reconcile the genuine new head or owner transition.

An underpayment or overpayment is rejected for resource delivery even if its borrow spend is accepted on-chain. If a transaction is conclusively never accepted, no KAS transfer occurred. If KAS moved but the quote, expiry, or finality policy fails, the head transition cannot be reversed by x402 and this binding provides no automatic refund or return path. A merchant may arrange a separate owner-authorized refund; clients MUST NOT assume one.

The minimum hosted finality is accepted, defined as trusted node evidence that the transaction is in accepted chain state, not merely the mempool. confirmed may be offered only with a documented stronger policy. On node disagreement, absent historical input evidence, uncertain acceptance or a reorg, settlement and grant issuance fail closed and retain recovery state. The hosted RC2 gateway offers accepted only, using configured PNN/WSS selected-chain evidence and durable pre-spend snapshots. It does not advertise the stronger confirmed policy or depend on a public REST transaction index. The issuer checks the current head in the virtual UTXO set before advertising a challenge and again before delivering the private grant.

Successful x402 response uses the standard transaction ID, network, payer and exact amount; extensions.kaspa adds this binding/profile, grant ID, head ID/version, covenant ID, previous and successor outpoints, output index 0, transaction encoding and observed finality. No secret grant material is returned.

Compatibility and release gates

The active standard-native, KIP-10 additive, and batch-settlement wire profiles retain their separate bindings. Old clients skip this new binding; new clients must opt in to its grant claim and upfront proof flow. An implementation MUST retain the existing x402 payment-identifier extension: the server advertises it, the payer echoes it, and settlement binds it to the normalized request and selected profile. An implementation MUST NOT advertise this profile until schemas, positive and negative vectors, client/server/facilitator/CLI coverage, transaction-v1 consensus vectors, durable grant recovery and funded Testnet-10 evidence meet the native-profile readiness bar.

RC2 also advertises paymentFlow: "upfront" on existing Kaspa exact offers that settle before the handler. The successor payTo net-gain interpretation requires upstream exact interoperability review before general advertising. Mainnet and stable release remain under their own readiness gates.

No cryptographic property here implies on-chain price or time enforcement. No private grant should be represented as revoked until a conflicting owner transition is accepted. The actual contract and network evidence must prove the specified behavior before the binding is promoted beyond its RC2 Testnet-10 interoperability candidate status.

Source: /spec/kaspa-hash-chain-exact-v1.md