For the complete documentation index, see llms.txt. This page is also available as Markdown.

Transaction Payload Signing

SwapKit can cryptographically sign the transaction payloads returned by the Swap API so integrators can verify that responses originate from SwapKit and have not been tampered with in transit. This page describes the generic ES256 signing flow that works for any swap, on any chain, with any token.

SLIP-0024 is a separate envelope format used specifically to render human-readable confirmation screens on hardware wallets. It is signed with a different scheme (ECDSA over secp256k1 of a binary-encoded payment request) and is not the signature described here. See the separate SLIP-0024 documentation.


Overview

  • Each API key can be associated with a secp256r1 (P-256 / ES256) key pair.

  • SwapKit holds the private key; integrators receive the public key when the key pair is created.

  • When a swap response includes a transaction, SwapKit signs it and returns, on each route's meta object:

    • meta.signedTx β€” the JWS payload component (see below). It is not the raw transaction.

    • meta.signature β€” the ES256 signature, encoded as a Flattened JWS signature.

  • The signature is a Flattened JWS (RFC 7515): SwapKit computes the SHA-256 digest of the canonical transaction, base64url-encodes that digest as the JWS payload (meta.signedTx), and signs the JWS Signing Input. The integrator verifies meta.signature against the reconstructed JWS Signing Input using the stored public key.

If no key pair is configured for the API key β€” or the response does not include a transaction β€” the swap response is returned unsigned and meta.signedTx / meta.signature are omitted.

The signature covers a digest of the transaction, not the transaction directly. Verifying the signature is necessary but not sufficient β€” see Verify the signature for the two checks you must perform.

Activate signing for your API key

Signing is activated by SwapKit on your behalf β€” there is no self-serve endpoint. To enable it, contact your SwapKit account manager and request a signing key pair for your API key.

Once provisioned, you will receive a public key in PEM format, for example:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----

Notes:

  • Store the public key securely on your side β€” this is the only value you need to verify signatures.

  • SwapKit holds the corresponding private key. It is never exposed to integrators and is stored encrypted at rest using Google Cloud KMS under a per-tenant encryption key.

  • A key pair cannot be overwritten in place. To rotate, request a rotation through your account manager.

  • secp256r1 (ES256) is the current default. Other key types may be added in the future.

Receive signed swap responses

Once signing is active, a swap response that includes a transaction carries the signature in each route's meta:

Neither field is the raw transaction: meta.signedTx is the JWS payload β€” BASE64URL(SHA-256 hex digest of the tx) β€” and meta.signature is the JWS signature over the JWS Signing Input eyJhbGciOiJFUzI1NiJ9.<meta.signedTx>. The exact byte layout of each field is in the Signature specification below.

Verify the signature

Verification has two independent checks β€” both are required:

  1. Signature check β€” the JWS signature is valid for the JWS Signing Input under your public key. This proves SwapKit produced the signature.

  2. Binding check β€” meta.signedTx equals BASE64URL(SHA-256-hex(your tx)). This proves the signature is bound to the transaction you are about to broadcast, not some other transaction.

Skipping step 2 leaves you exposed: an attacker could leave a valid signedTx/signature pair untouched while swapping out tx, and a signature-only check would still pass.

You do not need a SwapKit-specific SDK. The simplest correct approach is a JWS/JOSE library, because the signature is a Flattened JWS. A raw ES256 verifier also works if you reconstruct the JWS Signing Input and handle the signature encoding (see Recommended libraries).

Signature specification

Field
Value

Envelope

Flattened JWS JSON Serialization (RFC 7515 Β§7.2.2)

Curve

secp256r1 (also known as P-256 / prime256v1)

Hash

SHA-256

Algorithm

ECDSA (ES256, RFC 7518)

Public key format

PEM, SubjectPublicKeyInfo (as delivered on activation)

Protected header

{"alg":"ES256"} β†’ base64url eyJhbGciOiJFUzI1NiJ9

Signed bytes (JWS Signing Input)

eyJhbGciOiJFUzI1NiJ9.<meta.signedTx> (ASCII)

meta.signedTx (JWS payload)

BASE64URL(SHA-256 hex digest of the canonical tx string)

Signature encoding

JOSE: raw r || s, 64 bytes (concatenated, fixed-width). Not DER.

Signature transport encoding

base64url (meta.signature) β€” not standard base64, not hex

TypeScript example using jose library

Serialization note for the binding check. SwapKit computes the digest over JSON.stringify(tx) (for object transactions) of the same tx it returns, so re-stringifying the tx you received reproduces it as long as key order is preserved (JSON.parse β†’ JSON.stringify preserves insertion order in JS). In languages that do not preserve or canonicalize key order, compute the digest over the exact JSON bytes you received for tx rather than over a re-encoded object.

Recommended libraries

The signature is a Flattened JWS with a r || s (non-DER) signature, base64url-encoded. Pick one of two paths:

Path A β€” JWS/JOSE library (recommended). Hand it the protected header {"alg":"ES256"}, meta.signedTx as the payload, meta.signature, and your public key. It reconstructs the signing input and handles the r || s encoding for you.

  • Node.js / Browser β€” jose: jose.flattenedVerify({ protected, payload, signature }, key). This is what SwapKit uses; see the example above.

  • Python β€” jwcrypto or joserfc (both actively maintained). Avoid python-jose β€” it is effectively unmaintained and has had CVEs.

  • Go β€” go-jose (jose.ParseSigned / JSONWebSignature.Verify)

  • Java β€” nimbus-jose-jwt (JWSObject / ECDSAVerifier)

  • Rust β€” josekit. (Note: the jsonwebtoken crate only handles compact JWT, not arbitrary Flattened JWS payloads.)

Path B β€” raw ES256 verifier. If you use a generic ECDSA-P256 verifier instead, you must: (a) build the signing input string eyJhbGciOiJFUzI1NiJ9.<meta.signedTx> and pass its UTF-8 bytes as the data; (b) base64url-decode meta.signature to the raw 64-byte r || s; and (c) match the signature format your verifier expects. The verifier applies SHA-256 to the data itself.

Verifier

Signature format expected

Conversion from base64url r || s

raw r || s via { key, dsaEncoding: "ieee-p1363" }

none (default "der" would reject it)

WebCrypto SubtleCrypto.verify ({ name: "ECDSA", hash: "SHA-256" })

raw r || s

none

DER

encode_dss_signature(r, s), then public_key.verify(der, input, ec.ECDSA(hashes.SHA256()))

two big integers

split r/s, ecdsa.Verify(pub, sha256.Sum256(input)[:], r, s) (or DER + VerifyASN1)

DER

convert r || s to DER first

Whichever path you choose, the binding check (recomputing the digest from tx and comparing to meta.signedTx) is the same and still required.

What to do on verification failure

If either check fails:

  • Do not broadcast the transaction.

  • Do not retry against a different endpoint or relax the check.

  • Treat the response as untrusted and surface the error to the caller or log it for investigation.

A failure means the response did not come from SwapKit, was modified in transit, or the integration is using a stale public key after a rotation.


FAQ

Why do I have to recompute the digest if the signature already verifies? Because the signature only proves the digest is authentic. Without comparing signedTx to the digest of your tx, a tampered transaction with an intact (but unrelated) signedTx/signature pair would pass the signature check.

Does signing work for tokens that aren't in SLIP-0044? Yes. The ES256 signature is over a digest of the raw transaction payload SwapKit returns. It is independent of any token registry β€” there is no SLIP-0044 lookup involved, and token coverage is not limited by it.

Can I have multiple key pairs per API key? No. One key pair per API key. To rotate, request a rotation through your SwapKit point of contact.

Last updated