Skip to content

feat(x402-server): Casper Network support (CEP-18 via an x402 facilitator) - #54

Open
mssteuer wants to merge 4 commits into
altananetwork:stagingfrom
mssteuer:feat/casper-x402-network
Open

feat(x402-server): Casper Network support (CEP-18 via an x402 facilitator)#54
mssteuer wants to merge 4 commits into
altananetwork:stagingfrom
mssteuer:feat/casper-x402-network

Conversation

@mssteuer

Copy link
Copy Markdown

What & why

Adds Casper Network as a supported rail to @altananetwork/x402-server, so a
merchant can charge for a resource in CEP-18 tokens (wCSPR being the obvious
one) on casper:casper (mainnet) or casper:casper-test (testnet).

Today the seller side is EVM-only by construction. buildChallenge hardcodes
network = \eip155:${cfg.chainId}`(challenge.ts:19), the decoder validates every party as a 20-byte address and only parseseip155:out of the envelope (decode.ts:4, decode.ts:30-37),verifyPaymentbuildsviemEIP-712 typed data and checks it via ERC-1271 (verify.ts:88-133), andsettlePaymentencodes EVM calldata fortransferWithAuthorization/Permit2` (settle.ts). None of that survives contact with Casper: accounts are
ed25519/secp256k1 public keys and 32-byte account hashes, the asset is a CEP-18
contract package hash, and settlement is a Casper transaction rather than
calldata.

Rather than widening those viem-typed paths — which would mean Address
becoming string and a lot of if (isCasper) inside the EVM code — this adds
a parallel, self-contained rail in src/casper.ts. The EVM modules are not
touched at all (git diff --stat shows zero changes to challenge/decode/verify/
settle/merchant/tokens/types).

Verification and settlement are delegated over HTTP to an x402 facilitator for
Casper. CSPR.cloud runs a public one at https://x402-facilitator.cspr.cloud;
this PR implements its documented /verify, /settle and /supported
request/response shapes
(docs). Practically
that means the merchant needs no Casper node connectivity and holds no Casper
key — the facilitator checks the EIP-712 TransferAuthorization signature
against the payer's public key and submits the CEP-18
transfer_with_authorization transaction, paying the gas.

New public API (all re-exported from the package root):

Export Role
createCasperX402Merchant mirrors createX402Merchant: challengeBody / requirePayment / guard
buildCasperChallenge, casperPaymentRequirements 402 body + the paymentRequirements the facilitator validates against
decodeCasperPayment PAYMENT-SIGNATURE / X-PAYMENT envelope → typed payment
checkCasperPayment local business rules (network, asset, payTo, price, expiry) before a round-trip
createCasperFacilitator HTTP client for the facilitator; fetch is injectable
CASPER_MAINNET, CASPER_TESTNET, CASPER_FACILITATOR_URL constants

A few details worth calling out, because they came out of the facilitator's
documented error codes rather than guesswork:

  • buildCasperChallenge refuses maxTimeoutSeconds < 6 and
    checkCasperPayment returns insufficient_time when under 6 seconds of
    validity remain — the facilitator rejects both (insufficient_time).
  • extra.name / extra.version are always emitted: the facilitator rebuilds
    the EIP-712 domain from them and answers missing_token_name /
    missing_token_version otherwise.
  • /settle always replies HTTP 200 with a success boolean, so a failed
    settlement is turned into a thrown error carrying errorReason, and the
    merchant answers 402 with it.
  • Local reason strings reuse the facilitator's machine-readable codes
    (network_mismatch, pay_to_mismatch, amount_mismatch, …) so a merchant
    sees one vocabulary regardless of which side rejected the payment.
  • The wCSPR package hash is configuration, not a constant: it differs per
    network and I did not want to bake an unverified hash into the SDK.

Docs: new "Casper" section in docs/pages/sdk/x402-server.mdx and the package
README.

How was this tested?

New unit tests in packages/x402-server/src/casper.test.ts (20 tests) covering
the challenge shape on both Casper networks, price clamping, the sub-6s
rejection, envelope decode + round-trip back into the facilitator
paymentPayload, rejection of malformed input (non-base64, an eip155:
network, a 20-byte EVM address where an account hash belongs, a short nonce),
every local rejection reason, the facilitator client against a stubbed fetch
asserting the exact documented /verify and /settle bodies and headers, and
the merchant flow end to end (402 challenge → settle → receipt, verify-first
mode, replay refusal, settlement failure surfacing the facilitator's reason,
guard() reading PAYMENT-SIGNATURE).

Two of those tests exist specifically to prove the EVM side is untouched:
buildChallenge still emits eip155:56 with assetTransferMethod: "eip3009",
and decodeXPayment still rejects a Casper envelope.

The facilitator is not contacted over the network in tests — fetch is
injected — so the suite is hermetic. The request/response shapes asserted are
the ones documented at docs.cspr.cloud; I have not run a payment against
mainnet.

$ cd packages/x402-server && bun test src/casper.test.ts
 20 pass
 0 fail
 59 expect() calls
Ran 20 tests across 1 file. [47.00ms]

$ bun test src/merchant.test.ts        # existing suite, unchanged
 22 pass
 0 fail
 57 expect() calls
Ran 22 tests across 1 file. [1292.00ms]

$ bun run typecheck                    # repo root, all workspaces
@altananetwork/hypersigner-keystore-mcp typecheck: Exited with code 0
@altananetwork/sdk typecheck: Exited with code 0
@altananetwork/docs typecheck: Exited with code 0
@altananetwork/mcp typecheck: Exited with code 0
@altananetwork/passkey-crosschain typecheck: Exited with code 0
@altananetwork/passkey-demo typecheck: Exited with code 0
@altananetwork/x402-server typecheck: Exited with code 0
@altananetwork/e2e typecheck: Exited with code 0

$ bun run build                        # repo root
@altananetwork/x402-server build: Exited with code 0
@altananetwork/docs build: Exited with code 0

Per CONTRIBUTING.md the wallet package's tests were run one file at a time; no
wallet sources are touched by this PR.

Anything breaking?

No. Nothing existing changes behavior — src/index.ts gains exports and no
existing export's type or semantics move. The EVM rail files are byte-identical
to staging. The only non-additive edits are the version bumps required by the
changelog convention.

  • This PR is based on staging (see CONTRIBUTING.md)
  • User-visible changes have a CHANGELOG.md entry under the pending ## [x.y.z] - Unreleased section, and the target version reflects the change (fix → patch, feature → minor; see CONTRIBUTING.md) — or this is internal-only (CI/docs/tooling)

The pending section did not exist yet, so following CONTRIBUTING.md this PR
creates ## [0.9.0] - Unreleased (a feature starts a minor) and bumps the
publishable versions to match: @altananetwork/sdk and @altananetwork/mcp to
0.9.0 (with mcp's sdk range following), and @altananetwork/x402-server
the package that actually changed — to 0.3.0, noted under the heading the way
@altananetwork/mcp's divergent versions are noted in earlier entries. Happy
to drop the bumps if you'd rather do them at release time.

@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Functor Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant