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.
- The merchant privately assigns the current link and key to one payer's request through the grant claim.
- The payer uses that key to sign the head spend, adds its own funding, and independently broadcasts the payment.
- 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:
ownerRotate: owner SIGHASH_ALL authorization, one same-ID successor with a newly committed guard and head value at least the old head value. The owner funds any fee separately. Rotation replaces an abandoned or exhausted chain.ownerSweep: owner SIGHASH_ALL authorization with no same-ID successor; this terminates the head. A new head requires new genesis and a new offer.
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
- The merchant advertises the current public head/link terms. A payer authenticates and claims the one live private grant.
- The payer builds, signs and broadcasts the complete transaction, then presents the transaction artifact and request authorization in x402.
/settlechecks 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.- 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.
- Once finality is met, an atomic store consumes the canonical network
transaction ID, grant ID, expected head/version, challenge and
payment-identifierfor the one normalized request. The accepted head successor is recorded durably before protected work. - 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.