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#
| Package | Description |
|---|---|
com.okx:x402-java-core | Core: OKXFacilitatorClient, PaymentProcessor, PaymentHooks, AcceptOption, AssetRegistry, OKXEvmSigner, model layer (PaymentRequirements / PaymentPayload / VerifyResponse / SettleResponse, etc.). No servlet dependency. |
com.okx:x402-java-jakarta | Jakarta EE 9+ / Spring Boot 3 adapters: PaymentFilter (jakarta.servlet.Filter), PaymentInterceptor (Spring 6 HandlerInterceptor). |
com.okx:x402-java-javax | Java 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.
<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.
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:
| Form | Example | Behavior |
|---|---|---|
| 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#
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.
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):
| key | Meaning |
|---|---|
name | EIP-712 domain name (e.g. USD₮0) |
version | EIP-712 domain version (e.g. 1) |
transferMethod | eip3009 |
PaymentRequired (402 response body)#
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)#
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:
{
"signature": "0x...",
"authorization": {
"from": "0xBuyerEOA",
"to": "0xSellerEOA",
"value": "10000",
"validAfter": "0",
"validBefore": "1700000000",
"nonce": "0x..."
}
}
aggr_deferred payload map shape:
{
"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#
public class VerifyResponse {
public boolean isValid;
public String invalidReason;
public String invalidMessage;
public String payer;
public Map<String, Object> extensions;
}
SettleResponse#
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#
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)#
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/@ResponseBodyand you need thePAYMENT-RESPONSEproof header, you must usePaymentFilter(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#
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:
@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.
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#
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)#
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 = true → IllegalStateException 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.*;.
| Hook | Signature | Return-value meaning |
|---|---|---|
onBeforeVerify | (PaymentPayload, PaymentRequirements) -> AbortResult | proceed() continues; abort(reason) skips verify and returns HTTP 402 |
onAfterVerify | (PaymentPayload, PaymentRequirements, VerifyResponse) -> void | Observe-only — metrics / audit |
onVerifyFailure | (PaymentPayload, PaymentRequirements, Exception) -> RecoverResult<VerifyResponse> | notRecovered() rethrows; recovered(VerifyResponse) takes over the return value |
onBeforeSettle | (PaymentPayload, PaymentRequirements) -> AbortResult | Same as onBeforeVerify |
onAfterSettle | (PaymentPayload, PaymentRequirements, SettleResponse) -> void | Observe-only |
onSettleFailure | (PaymentPayload, PaymentRequirements, Exception) -> RecoverResult<SettleResponse> | Same as onVerifyFailure |
onAsyncSettleComplete | (PaymentPayload, PaymentRequirements, SettleResponse, Throwable) -> void | Invoked only when asyncSettle=true |
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).
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.
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:
| Adapter | Timing | PAYMENT-RESPONSE proof header | Use case |
|---|---|---|---|
PaymentInterceptor | Spring MVC postHandle | Works 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 |
PaymentFilter | servlet Filter, wraps a BufferedHttpServletResponse | Always preserved | @RestController JSON APIs, when you need the proof header |
Practice: default to
PaymentFilterfor Spring REST APIs; usePaymentInterceptoronly 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.
@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.
@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)#
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:
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.
| Field | Value |
|---|---|
RouteConfig.scheme | "exact" |
payload.authorization.from | Buyer EOA address |
payload.authorization.validBefore | now + maxTimeoutSeconds |
SettleResponse.transaction | Real tx hash |
SettleResponse.status | "success" / "pending" / "timeout" |
Buyer side uses OKXEvmSigner (EIP-3009 + EIP-712 signing, web3j-based):
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.
| Field | Value |
|---|---|
RouteConfig.scheme | "aggr_deferred" |
payload.authorization.from | AA wallet address (not the session key address) |
payload.authorization.validBefore | uint256.max (no expiry) |
accepted.extra.sessionCert | OKX 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:
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(...).