SDK reference

Java SDK Reference#

Targets Java 17+, published to Maven Central as three artifacts: com.okx:x402-java-core (servlet-agnostic core), com.okx:x402-java-jakarta (Jakarta EE 9+ / Spring Boot 3), com.okx:x402-java-javax (Java EE 8 / Spring Boot 2). Source: github.com/okx/payments/tree/main/java.

1. Packages#

PackageDescription
com.okx:x402-java-coreCore: OKXFacilitatorClient, PaymentProcessor, PaymentHooks, AcceptOption, AssetRegistry, OKXEvmSigner, model layer (PaymentRequirements / PaymentPayload / VerifyResponse / SettleResponse, etc.). No servlet dependency.
com.okx:x402-java-jakartaJakarta EE 9+ / Spring Boot 3 adapters: PaymentFilter (jakarta.servlet.Filter), PaymentInterceptor (Spring 6 HandlerInterceptor).
com.okx:x402-java-javaxJava EE 8 / Spring Boot 2 adapters: same as above but based on javax.servlet.* + Spring 5.

Install jakarta or javax — not both. They expose the same package names and would conflict.

xml
<dependency>
  <groupId>com.okx</groupId>
  <artifactId>x402-java-jakarta</artifactId>   <!-- or x402-java-javax -->
  <version>1.0.0</version>
</dependency>

For non-servlet frameworks (Vert.x / Play / Micronaut Netty), depend only on x402-java-core and implement the X402Request / X402Response SPIs (~50 lines).

2. Core types#

Network#

CAIP-2 string. Currently only eip155:196 (X Layer mainnet) is supported.

java
String network = "eip155:196";

Price / asset#

The Java SDK has no separate Money / Price / AssetAmount types — RouteConfig.price is a String with three accepted forms:

FormExampleBehavior
USD string"$0.01"Auto-converted to the corresponding token's atomic units via AssetRegistry
Numeric string"0.01"Same as USD string
Atomic-unit string"10000" (route.asset must be set explicitly)Used directly as the token amount, no further conversion

For multi-token / multi-scheme cases, use the AcceptOption list (see §3).

ResourceInfo#

java
public class ResourceInfo {
    public String url;          // Resource URL
    public String description;  // Description
    public String mimeType;     // MIME
}

PaymentRequirements#

One entry of accepts[] in the 402 envelope.

java
public class PaymentRequirements {
    public String scheme;             // "exact" | "aggr_deferred"
    public String network;            // "eip155:196"
    public String amount;             // Atomic-unit string
    public String payTo;              // Recipient EOA
    public int    maxTimeoutSeconds;  // Signature validity
    public String asset;              // Token contract address
    public Map<String, Object> extra; // Scheme-specific fields
}

Fields in extra for exact (EIP-3009):

keyMeaning
nameEIP-712 domain name (e.g. USD₮0)
versionEIP-712 domain version (e.g. 1)
transferMethodeip3009

PaymentRequired (402 response body)#

java
public class PaymentRequired {
    public int x402Version = 2;
    public String error;
    public ResourceInfo resource;
    public List<PaymentRequirements> accepts;
    public Map<String, Object> extensions;
}

PaymentPayload (carried in the PAYMENT-SIGNATURE header)#

java
public class PaymentPayload {
    public int x402Version = 2;
    public ResourceInfo resource;
    public PaymentRequirements accepted;     // The selected accepts[i]
    public Map<String, Object> payload;      // Scheme-specific signed payload
    public Map<String, Object> extensions;

    public String toHeader();                          // base64(JSON)
    public static PaymentPayload fromHeader(String);   // Reverse decode
}

exact (EIP-3009) payload map shape:

json
{
  "signature": "0x...",
  "authorization": {
    "from": "0xBuyerEOA",
    "to":   "0xSellerEOA",
    "value": "10000",
    "validAfter":  "0",
    "validBefore": "1700000000",
    "nonce": "0x..."
  }
}

aggr_deferred payload map shape:

json
{
  "signature": "0x...",
  "authorization": {
    "from": "0xAAWalletAddress",
    "to":   "0xSellerEOA",
    "value": "10000",
    "validAfter":  "0",
    "validBefore": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
    "nonce": "0x..."
  }
}

accepted.extra.sessionCert carries the OKX Wallet TEE-issued session certificate (Base64) for aggr_deferred.

VerifyResponse#

java
public class VerifyResponse {
    public boolean isValid;
    public String  invalidReason;
    public String  invalidMessage;
    public String  payer;
    public Map<String, Object> extensions;
}

SettleResponse#

java
public class SettleResponse {
    public boolean success;
    public String  errorReason;
    public String  errorMessage;
    public String  payer;
    public String  transaction;          // exact: tx hash; aggr_deferred: ""
    public String  network;
    public String  amount;               // upto scheme: actual settled amount
    public String  status;               // "pending" | "success" | "timeout"
    public Map<String, Object> extensions;
}

SupportedKind / SupportedResponse#

java
public class SupportedKind {
    public int x402Version = 2;
    public String scheme;
    public String network;
    public Map<String, Object> extra;
}

public class SupportedResponse {
    public List<SupportedKind> kinds;
    public List<String> extensions;
    public Map<String, List<String>> signers;
}

3. Server API (PaymentInterceptor / PaymentFilter)#

The server entry point is PaymentInterceptor (Spring MVC HandlerInterceptor adapter) or PaymentFilter (servlet Filter adapter). Both drive the same PaymentProcessor underneath (servlet-agnostic orchestrator handling verify → business handler → settle); use interceptor.processor() / filter.processor() to access the underlying PaymentProcessor for hooks and settleExecutor injection.

Construction (recommended — Spring Boot 3 / Jakarta)#

java
import com.okx.x402.server.PaymentInterceptor;
import com.okx.x402.server.PaymentProcessor;

PaymentInterceptor interceptor = PaymentInterceptor.create(
        facilitator,                         // FacilitatorClient
        Map.of("GET /api/data", route));     // routes

// Get the underlying processor to configure hooks / executor
interceptor.processor()
           .settleExecutor(settlePool)
           .onAfterSettle((p, r, resp) -> auditLog.write(resp));

Alternative: PaymentFilter.create(facilitator, routes). When the business route is @RestController / @ResponseBody and you need the PAYMENT-RESPONSE proof header, you must use PaymentFilter (see the PaymentInterceptor caveats below).

Low-level API: instantiating new PaymentProcessor(facilitator, routes) directly is reserved for non-servlet frameworks (Vert.x / Play / Netty, see §4).

RouteConfig#

java
public static class RouteConfig {
    public String  scheme            = "exact";   // "exact" | "aggr_deferred"
    public String  network;                       // REQUIRED: "eip155:196"
    public String  payTo;                         // REQUIRED: recipient EOA
    public String  price;                         // "$0.01" or "10000"
    public String  asset;                         // Empty → AssetRegistry default (USDT0)
    public int     maxTimeoutSeconds = 86400;     // Signature validity, default 1 day
    public DynamicPrice priceFunction;            // Dynamic pricing (computed per request)
    public List<AcceptOption> accepts;            // Used for multi-token / multi-scheme; overrides scheme/price/asset
    public boolean syncSettle;                    // Wait for on-chain confirmation before returning
    public boolean asyncSettle;                   // Background settle, requires settleExecutor
}

DynamicPrice functional interface:

java
@FunctionalInterface
public interface DynamicPrice {
    String resolve(X402Request request);   // Returns USD/atomic-unit string
}

AcceptOption#

For multi-token / multi-scheme, fill route.accepts; each AcceptOption becomes one entry in the 402 envelope.

java
public class AcceptOption {
    public String scheme;
    public String network;             // Empty inherits route.network
    public String payTo;               // Empty inherits route.payTo
    public String price;
    public DynamicPrice priceFunction;
    public String asset;               // Empty → AssetRegistry default
    public int    maxTimeoutSeconds;
    public Map<String, Object> extra;

    public static Builder builder();   // Chainable
}

// Example
AcceptOption.builder()
    .scheme("exact").price("$0.01")
    .asset("0x4ae46a509f6b1d9056937ba4500cb143933d2dc8")   // USDG
    .build();

Polling parameters#

java
processor.pollInterval(Duration.ofSeconds(1));   // settle status poll interval, default 1s
processor.pollDeadline(Duration.ofSeconds(5));   // settle status poll timeout, default 5s

settleExecutor (required when asyncSettle is enabled)#

java
ExecutorService settlePool = Executors.newFixedThreadPool(16, r -> {
    Thread t = new Thread(r, "x402-settle"); t.setDaemon(true); return t;
});
processor.settleExecutor(settlePool);

If not injected and route.asyncSettle = trueIllegalStateException is thrown at runtime. The SDK does not silently spawn background threads.

Server lifecycle hooks#

Hook result types live as inner classes of com.okx.x402.server.PaymentHooks: PaymentHooks.AbortResult / PaymentHooks.RecoverResult<T> / PaymentHooks.ProtectedRequestResult / PaymentHooks.SettlementTimeoutResult. The examples below assume import static com.okx.x402.server.PaymentHooks.*;.

HookSignatureReturn-value meaning
onBeforeVerify(PaymentPayload, PaymentRequirements) -> AbortResultproceed() continues; abort(reason) skips verify and returns HTTP 402
onAfterVerify(PaymentPayload, PaymentRequirements, VerifyResponse) -> voidObserve-only — metrics / audit
onVerifyFailure(PaymentPayload, PaymentRequirements, Exception) -> RecoverResult<VerifyResponse>notRecovered() rethrows; recovered(VerifyResponse) takes over the return value
onBeforeSettle(PaymentPayload, PaymentRequirements) -> AbortResultSame as onBeforeVerify
onAfterSettle(PaymentPayload, PaymentRequirements, SettleResponse) -> voidObserve-only
onSettleFailure(PaymentPayload, PaymentRequirements, Exception) -> RecoverResult<SettleResponse>Same as onVerifyFailure
onAsyncSettleComplete(PaymentPayload, PaymentRequirements, SettleResponse, Throwable) -> voidInvoked only when asyncSettle=true
java
processor
    .onBeforeVerify((p, r) -> AbortResult.proceed())
    .onAfterVerify((p, r, resp) -> metrics.verifyOk())
    .onVerifyFailure((p, r, e) -> RecoverResult.<VerifyResponse>notRecovered())
    .onBeforeSettle((p, r) -> AbortResult.proceed())
    .onAfterSettle((p, r, resp) -> auditLog.write(resp))
    .onSettleFailure((p, r, e) -> RecoverResult.<SettleResponse>notRecovered());

HTTP-layer hook: onProtectedRequest(hook)#

Fires after route matching and before reading the payment header. Use it to skip payment (allowlist) or hard-reject (rate-limit).

java
import static com.okx.x402.server.PaymentHooks.ProtectedRequestResult;

processor.onProtectedRequest((request, routeConfig) -> {
    if ("internal".equals(request.getHeader("x-api-key"))) {
        return ProtectedRequestResult.grantAccess();    // Skip payment, proceed to business
    }
    if (rateLimiter.isThrottled(request)) {
        return ProtectedRequestResult.abort("rate_limited");  // HTTP 403, {"error":"rate_limited"}
    }
    return ProtectedRequestResult.proceed();             // Normal payment flow
});

Multiple hooks run in registration order; the first to return grantAccess() / abort(...) wins.

Fallback hook: onSettlementTimeout(hook)#

Fires when facilitator settle-status polling exceeds pollDeadline without reaching a terminal state (single hook: later registration replaces earlier). Useful for fallback on-chain confirmation against your own RPC.

java
import static com.okx.x402.server.PaymentHooks.SettlementTimeoutResult;

processor.onSettlementTimeout((txHash, network) -> {
    TransactionReceipt r = web3j.ethGetTransactionReceipt(txHash)
            .send().getTransactionReceipt().orElse(null);
    return (r != null && r.isStatusOK())
            ? SettlementTimeoutResult.confirmed()       // Confirmed on-chain → treat as success
            : SettlementTimeoutResult.notConfirmed();   // Fall through to original timeout 402 flow
});

PaymentInterceptor vs PaymentFilter#

The two have equivalent signatures (create(facilitator, routes) + .processor() to grab the underlying PaymentProcessor); the route key format is the same "METHOD /path". The difference is response timing:

AdapterTimingPAYMENT-RESPONSE proof headerUse case
PaymentInterceptorSpring MVC postHandleWorks for @Controller returning a view name; on @RestController / @ResponseBody paths it is silently dropped (response already committed)Business uses view templates, or proof header isn't needed
PaymentFilterservlet Filter, wraps a BufferedHttpServletResponseAlways preserved@RestController JSON APIs, when you need the proof header

Practice: default to PaymentFilter for Spring REST APIs; use PaymentInterceptor only when the host already has an interceptor chain and you've confirmed you don't need the proof header.

4. Middleware reference#

Four host-framework integration paths, all backed by PaymentFilter.create(...) / PaymentInterceptor.create(...).

Spring Boot 3 (Jakarta)#

Register via FilterRegistrationBean for clean ordering relative to billing / auth filters.

java
@Bean
FilterRegistrationBean<PaymentFilter> x402Filter(OKXFacilitatorClient facilitator) {
    PaymentProcessor.RouteConfig route = new PaymentProcessor.RouteConfig();
    route.network = "eip155:196";
    route.payTo   = System.getenv("PAY_TO_ADDRESS");
    route.price   = "$0.01";

    FilterRegistrationBean<PaymentFilter> reg = new FilterRegistrationBean<>(
            PaymentFilter.create(facilitator, Map.of("GET /api/data", route)));
    reg.addUrlPatterns("/api/*");
    reg.setOrder(20);          // billing filter at 10
    return reg;
}

Spring Boot 2 (Javax)#

Source code is identical — swap the dependency to com.okx:x402-java-javax. The package name com.okx.x402.server.PaymentFilter does not change.

Spring MVC HandlerInterceptor#

Prefer this when the host already uses an interceptor chain — InterceptorRegistry.order() is more intuitive than mixing filters and interceptors.

java
@Configuration
class X402Config implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry r) {
        r.addInterceptor(billingInterceptor).order(10);
        r.addInterceptor(PaymentInterceptor.create(facilitator, routes))
                .order(20)
                .addPathPatterns("/api/**");
    }
}

@RestController / @ResponseBody paths lose the PAYMENT-RESPONSE proof header (see §3 "PaymentInterceptor vs PaymentFilter"); @Controller returning a view name and async / streaming controllers that haven't committed are unaffected.

Bare Servlet (Jetty / Tomcat)#

java
public class App implements ServletContextInitializer {
    @Override
    public void onStartup(ServletContext ctx) {
        ctx.addFilter("x402", PaymentFilter.create(facilitator, routes))
           .addMappingForUrlPatterns(null, false, "/api/*");
    }
}

For embedded Jetty use ServletContextHandler.addFilter(...); Tomcat uses Context.addFilterDef + addFilterMap.

Non-Servlet (Vert.x / Play / Netty)#

Depend only on x402-java-core, implement two SPIs:

java
class VertxX402Request  implements X402Request  { /* ~25 lines */ }
class VertxX402Response implements X402Response { /* ~25 lines */ }

PaymentProcessor processor = new PaymentProcessor(facilitator, routes);

// preHandle returning null = response already written (402 / 500), caller should short-circuit
// returning non-null but !isVerified() = not a paid route (PASS_THROUGH), fall to business handler
PaymentProcessor.VerifyResult vr = processor.preHandle(xReq, xRes);
if (vr == null) return;                              // 402 / 500 already written
// ... business handler ...
if (vr.isVerified()) {
    processor.postHandle(vr, xReq, xRes);            // Triggers settle + writes PAYMENT-RESPONSE
}

The jakarta adapter is under 100 lines total — a useful reference implementation.

5. Mechanism types (EVM Schemes)#

In the Java SDK, scheme is a string — there's no separate ExactEvmScheme / AggrDeferredEvmScheme class. Scheme behavior is determined jointly by RouteConfig.scheme and the facilitator-side implementation.

exact (instant single settlement)#

The EOA private key signs an EIP-3009 TransferWithAuthorization; the facilitator submits on-chain immediately.

FieldValue
RouteConfig.scheme"exact"
payload.authorization.fromBuyer EOA address
payload.authorization.validBeforenow + maxTimeoutSeconds
SettleResponse.transactionReal tx hash
SettleResponse.status"success" / "pending" / "timeout"

Buyer side uses OKXEvmSigner (EIP-3009 + EIP-712 signing, web3j-based):

java
OKXEvmSigner signer  = new OKXEvmSigner(System.getenv("PRIVATE_KEY"));
OKXHttpClient client = new OKXHttpClient(signer, "eip155:196");
HttpResponse<String> resp = client.get(URI.create("https://seller/api/data"));
// SDK auto-handles 402 → sign → replay → 200

aggr_deferred (batch deferred settlement)#

The Buyer signs with the session private key (not the EOA); the OKX Facilitator TEE compresses N payments into a single on-chain tx — suitable for AI Agent batch payments.

FieldValue
RouteConfig.scheme"aggr_deferred"
payload.authorization.fromAA wallet address (not the session key address)
payload.authorization.validBeforeuint256.max (no expiry)
accepted.extra.sessionCertOKX Wallet TEE-issued Base64 session certificate
SettleResponse.transaction"" (empty string — TEE merges asynchronously on-chain)
SettleResponse.status"success" (indicates entry into the batch)

Seller side: identical to exact, only route.scheme = "aggr_deferred".

Buyer side: OKXEvmSigner only supports EOA private keys and does not directly support aggr_deferred. Coordinate with the OKX Wallet team to obtain a session signer implementing the EvmSigner interface.

Asset configuration (AssetRegistry / AssetConfig)#

X Layer USDT0 is pre-registered by default (0x779ded0c9e1022225f8e0630b35a9b54be713736, 6 decimals, EIP-712 name USD₮0 U+20AE). Other EIP-3009 assets must be registered explicitly:

java
AssetRegistry.register("eip155:196", AssetConfig.builder()
        .symbol("USDG")
        .contractAddress("0x4ae46a509f6b1d9056937ba4500cb143933d2dc8")
        .decimals(6)
        .eip712Name("USDG")
        .eip712Version("1")
        .transferMethod("eip3009")
        .build());

Custom assets must be registered before PaymentFilter.create(...) / PaymentInterceptor.create(...).