Documentation Index Fetch the complete documentation index at: https://developers.fd.xyz/llms.txt
Use this file to discover all available pages before exploring further.
Coming soon. The org.fdtech.prism:prism-servlet artifact is not yet published to Maven Central. This page describes the planned API.
Overview
The prism-servlet package provides a generic Servlet Filter for payment-protecting Java web applications using the x402 protocol. It works with any servlet-based framework (Tomcat, Jetty, Spring Boot, Jakarta EE, etc.).
Framework Agnostic Works with any Servlet 3.0+ container
Response Buffering Captures output before settlement validation
Standard Filter Standard javax.servlet.Filter interface
Installation
< dependency >
< groupId >org.fdtech.prism</ groupId >
< artifactId >prism-servlet</ artifactId >
< version >1.0.0</ version >
</ dependency >
implementation 'org.fdtech.prism:prism-servlet:1.0.0'
The canonical SDK config is identifyToken (PRISM_IDENTIFY_TOKEN env var) — formerly apiKey / PRISM_API_KEY. SDKs accept the legacy names as fallback during the migration window; new code should use the canonical names.
Quick Start
Configuration via web.xml
<!-- web.xml -->
< filter >
< filter-name >PrismFilter</ filter-name >
< filter-class >org.fdtech.prism.servlet.PrismFilter</ filter-class >
< init-param >
< param-name >identifyToken</ param-name >
< param-value >dev-key-123</ param-value >
</ init-param >
< init-param >
< param-name >baseUrl</ param-name >
< param-value >https://prism-gw.fd.xyz</ param-value >
</ init-param >
< init-param >
< param-name >routes</ param-name >
< param-value >
/api/premium:0.01:Premium API access
/api/weather:$0.001:Weather data access
</ param-value >
</ init-param >
</ filter >
< filter-mapping >
< filter-name >PrismFilter</ filter-name >
< url-pattern >/api/*</ url-pattern >
</ filter-mapping >
Servlet Implementation
import javax.servlet.ServletException ;
import javax.servlet.annotation.WebServlet ;
import javax.servlet.http. * ;
import java.io.IOException ;
@ WebServlet ( "/api/premium" )
public class PremiumServlet extends HttpServlet {
@ Override
protected void doGet ( HttpServletRequest request ,
HttpServletResponse response )
throws ServletException , IOException {
// Access payer address (set by PrismFilter)
String payer = (String) request . getAttribute ( "prism_payer" );
response . setContentType ( "application/json" );
response . getWriter (). write ( String . format (
"{ \" message \" : \" Premium content \" , \" payer \" : \" %s \" }" ,
payer
));
}
}
Configuration
PrismConfig
import org.fdtech.prism.core.PrismConfig ;
PrismConfig config = new PrismConfig (
"your-project-identify-token" , // Required: Project Identify Token
"https://prism-gw.fd.xyz" , // Optional: Base URL
true // Optional: Debug mode
) ;
Programmatic Configuration
import org.fdtech.prism.servlet.PrismFilter ;
import javax.servlet.FilterRegistration ;
// In ServletContextInitializer or similar
public class WebAppConfig {
public void configureFilter ( ServletContext context ) {
PrismConfig config = new PrismConfig ( "dev-key-123" );
PrismFilter filter = new PrismFilter (config);
// Add route configuration
filter . addRoute ( "/api/premium" , 0.01 , "Premium API" );
filter . addRoute ( "/api/weather" , "$0.001" , "Weather data" );
// Register filter
FilterRegistration . Dynamic registration =
context . addFilter ( "PrismFilter" , filter);
registration . addMappingForUrlPatterns (
null , false , "/api/*"
);
}
}
In web.xml
Routes are configured as colon-separated values, one per line:
<init-param>
<param-name>routes</param-name>
<param-value>
/api/premium:0.01:Premium API access
/api/weather:$0.001:Weather data
/api/data/*:0.005:Data API (wildcard)
</param-value>
</init-param>
Format: path:price:description
path : Exact path or wildcard (/api/*)
price : USD amount (0.01 = 0.01 U S D , o r ‘ 0.01 USD, or ` 0.01 U S D , or ‘ 0.001` string format)
description : Human-readable description
Programmatic Configuration
PrismFilter filter = new PrismFilter (config) ;
// Exact paths
filter . addRoute ( "/api/premium" , 0.01 , "Premium API" );
filter . addRoute ( "/api/weather" , "$0.001" , "Weather data" );
// Wildcard matching
filter . addRoute ( "/api/data/*" , 0.005 , "Data API" );
// Multiple route groups
filter . addRoute ( "/api/basic/*" , "$0.0001" , "Basic tier" );
filter . addRoute ( "/api/premium/*" , "$0.01" , "Premium tier" );
Payment info is stored as request attributes:
@ WebServlet ( "/api/premium" )
public class PremiumServlet extends HttpServlet {
@ Override
protected void doGet ( HttpServletRequest request ,
HttpServletResponse response )
throws ServletException , IOException {
// Get payer wallet address
String payer = (String) request . getAttribute ( "prism_payer" );
// Returns: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" or null
// Get full payment object
Map < String , Object > payment =
( Map < String, Object > ) request . getAttribute ( "prism_payment" );
/*
{
"scheme": "eip3009",
"network": "eth-sepolia",
"asset": "usdc",
"amount": "10000",
"recipient": "0x...",
"signature": "0x..."
}
*/
response . setContentType ( "application/json" );
response . getWriter (). write (
new JSONObject ()
. put ( "message" , "Premium content" )
. put ( "payer" , payer)
. put ( "network" , payment . get ( "network" ))
. toString ()
);
}
}
Safe Access Helper
public class PaymentUtils {
public static String getPayer ( HttpServletRequest request ) {
return (String) request . getAttribute ( "prism_payer" );
}
public static Map < String , Object > getPayment ( HttpServletRequest request ) {
return ( Map < String, Object > ) request . getAttribute ( "prism_payment" );
}
public static boolean hasPayer ( HttpServletRequest request ) {
return request . getAttribute ( "prism_payer" ) != null ;
}
}
// Usage in servlet
@ WebServlet ( "/api/data" )
public class DataServlet extends HttpServlet {
@ Override
protected void doGet ( HttpServletRequest request ,
HttpServletResponse response )
throws ServletException , IOException {
if ( ! PaymentUtils . hasPayer (request)) {
response . sendError ( 402 , "Payment required" );
return ;
}
String payer = PaymentUtils . getPayer (request);
response . setContentType ( "application/json" );
response . getWriter (). write (
"{ \" data \" :[1,2,3], \" payer \" : \" " + payer + " \" }"
);
}
}
Settlement Validation
The Java Servlet filter uses HttpServletResponseWrapper to capture servlet output before settlement validation:
// Internal implementation (automatic!)
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = new ByteArrayOutputStream () ;
private PrintWriter writer = new PrintWriter (buffer) ;
@ Override
public PrintWriter getWriter () {
return writer;
}
public byte [] getCapturedOutput () {
writer . flush ();
return buffer . toByteArray ();
}
}
// In PrismFilter.doFilter()
ResponseWrapper wrapper = new ResponseWrapper (response) ;
chain . doFilter (request, wrapper); // Execute servlet - output goes to buffer
// Validate settlement BEFORE sending to client
String settlementHeader = middleware . getSettlementHeader (paymentInfo);
if (settlementHeader == null ) {
// ❌ Settlement failed - send error instead of buffered output
response . setStatus ( 402 );
response . setContentType ( "application/json" );
response . getWriter (). write (
"{ \" error \" : \" Payment settlement failed \" }"
);
} else {
// ✅ Settlement succeeded - send buffered output
response . setHeader ( "X-PAYMENT-RESPONSE" , settlementHeader);
response . getOutputStream (). write ( wrapper . getCapturedOutput ());
}
Key Points:
Servlet writes to wrapper , not real response
Output is buffered in memory
After settlement check, either send buffered data or error
Works with all response types (JSON, HTML, binary, etc.)
See Stablecoin Settlement for details.
Error Handling
Payment Errors
When payment is missing or invalid:
HTTP/ 1.1 402 Payment Required
{
"x402Version" : 2 ,
"paymentRequired" : true ,
"acceptedPayments" : [
{
"scheme" : "eip3009" ,
"network" : "eth-sepolia" ,
"asset" : "usdc" ,
"amount" : "10000" ,
"recipient" : "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" ,
"nonce" : "0xabc123..." ,
"validBefore" : 1735430400
}
],
"description" : "Premium API access" ,
"priceUSD" : "0.01"
}
Custom Error Handling
@ WebServlet ( "/api/premium" )
public class PremiumServlet extends HttpServlet {
@ Override
protected void doGet ( HttpServletRequest request ,
HttpServletResponse response )
throws ServletException , IOException {
try {
String payer = (String) request . getAttribute ( "prism_payer" );
if (payer == null ) {
sendPaymentRequired (response);
return ;
}
// Process request
String result = processRequest (payer);
response . setContentType ( "application/json" );
response . getWriter (). write (result);
} catch ( Exception e ) {
sendError (response, e);
}
}
private void sendPaymentRequired ( HttpServletResponse response )
throws IOException {
response . setStatus ( 402 );
response . setContentType ( "application/json" );
response . getWriter (). write (
"{ \" error \" : \" Payment required \" }"
);
}
private void sendError ( HttpServletResponse response , Exception e )
throws IOException {
response . setStatus ( 500 );
response . setContentType ( "application/json" );
response . getWriter (). write (
"{ \" error \" : \" " + e . getMessage () + " \" }"
);
}
}
Spring Boot Integration
Configuration Class
import org.springframework.boot.web.servlet.FilterRegistrationBean ;
import org.springframework.context.annotation.Bean ;
import org.springframework.context.annotation.Configuration ;
import org.fdtech.prism.servlet.PrismFilter ;
import org.fdtech.prism.core.PrismConfig ;
@ Configuration
public class PrismConfig {
@ Bean
public FilterRegistrationBean < PrismFilter > prismFilter () {
PrismConfig config = new PrismConfig (
"dev-key-123" ,
"https://prism-gw.fd.xyz"
);
PrismFilter filter = new PrismFilter (config);
filter . addRoute ( "/api/premium" , 0.01 , "Premium API" );
filter . addRoute ( "/api/weather" , "$0.001" , "Weather data" );
FilterRegistrationBean < PrismFilter > registration =
new FilterRegistrationBean <>();
registration . setFilter (filter);
registration . addUrlPatterns ( "/api/*" );
registration . setOrder ( 1 ); // Execute early
return registration;
}
}
Spring Boot Controller
import org.springframework.web.bind.annotation. * ;
import javax.servlet.http.HttpServletRequest ;
import java.util.Map ;
@ RestController
@ RequestMapping ( "/api" )
public class ApiController {
@ GetMapping ( "/premium" )
public Map < String , Object > premium ( HttpServletRequest request ) {
String payer = (String) request . getAttribute ( "prism_payer" );
return Map . of (
"message" , "Premium content" ,
"payer" , payer
);
}
@ GetMapping ( "/weather" )
public Map < String , Object > weather ( HttpServletRequest request ) {
String payer = (String) request . getAttribute ( "prism_payer" );
return Map . of (
"location" , "San Francisco" ,
"temperature" , 72 ,
"condition" , "Sunny" ,
"payer" , payer
);
}
}
Testing
JUnit 5 Tests
import org.junit.jupiter.api.Test ;
import org.springframework.boot.test.context.SpringBootTest ;
import org.springframework.boot.test.web.client.TestRestTemplate ;
import org.springframework.http. * ;
import static org.junit.jupiter.api.Assertions. * ;
@ SpringBootTest ( webEnvironment = SpringBootTest . WebEnvironment . RANDOM_PORT )
class PaymentIntegrationTest {
@ Autowired
private TestRestTemplate restTemplate ;
@ Test
void testPaymentRequired () {
ResponseEntity < String > response =
restTemplate . getForEntity ( "/api/premium" , String . class );
assertEquals ( HttpStatus . PAYMENT_REQUIRED , response . getStatusCode ());
assertTrue ( response . getBody (). contains ( "paymentRequired" ));
}
@ Test
void testValidPayment () {
// Create mock payment header
String payment = "{ \" scheme \" : \" eip3009 \" , \" signature \" : \" 0x" +
"0" . repeat ( 130 ) + " \" }" ;
HttpHeaders headers = new HttpHeaders ();
headers . set ( "X-PAYMENT" , payment);
HttpEntity < String > entity = new HttpEntity <>(headers);
ResponseEntity < String > response = restTemplate . exchange (
"/api/premium" ,
HttpMethod . GET ,
entity,
String . class
);
// In test mode, mock signatures accepted
assertEquals ( HttpStatus . OK , response . getStatusCode ());
assertTrue ( response . getBody (). contains ( "Premium content" ));
}
}
Mock Servlet Testing
import org.junit.jupiter.api.BeforeEach ;
import org.junit.jupiter.api.Test ;
import org.mockito.Mock ;
import org.mockito.MockitoAnnotations ;
import javax.servlet.FilterChain ;
import javax.servlet.http.HttpServletRequest ;
import javax.servlet.http.HttpServletResponse ;
import static org.mockito.Mockito. * ;
class PrismFilterTest {
@ Mock
private HttpServletRequest request ;
@ Mock
private HttpServletResponse response ;
@ Mock
private FilterChain chain ;
private PrismFilter filter ;
@ BeforeEach
void setUp () {
MockitoAnnotations . openMocks ( this );
PrismConfig config = new PrismConfig ( "test-key" );
filter = new PrismFilter (config);
filter . addRoute ( "/api/premium" , 0.01 , "Test" );
}
@ Test
void testPaymentRequired () throws Exception {
when ( request . getRequestURI ()). thenReturn ( "/api/premium" );
when ( request . getHeader ( "X-PAYMENT" )). thenReturn ( null );
filter . doFilter (request, response, chain);
verify (response). setStatus ( 402 );
verify (chain, never ()). doFilter ( any (), any ());
}
}
Production Deployment
Environment Configuration
# application.properties (Spring Boot)
prism.identify-token = ${PRISM_IDENTIFY_TOKEN}
prism.base-url = ${PRISM_BASE_URL:https://prism-gw.fd.xyz}
prism.debug = ${PRISM_DEBUG:false}
# Routes configuration
prism.routes[0]. path = /api/premium
prism.routes[0]. price = 0.01
prism.routes[0]. description = Premium API
prism.routes[1]. path = /api/weather
prism.routes[1]. price = $0.001
prism.routes[1]. description = Weather data
Configuration Class
import org.springframework.boot.context.properties.ConfigurationProperties ;
import org.springframework.context.annotation.Configuration ;
@ Configuration
@ ConfigurationProperties ( prefix = "prism" )
public class PrismProperties {
private String identifyToken ;
private String baseUrl ;
private boolean debug ;
private List < RouteConfig > routes ;
// Getters and setters
public static class RouteConfig {
private String path ;
private String price ;
private String description ;
// Getters and setters
}
}
Logging & Monitoring
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
public class PremiumServlet extends HttpServlet {
private static final Logger logger =
LoggerFactory . getLogger ( PremiumServlet . class );
@ Override
protected void doGet ( HttpServletRequest request ,
HttpServletResponse response )
throws ServletException , IOException {
String payer = (String) request . getAttribute ( "prism_payer" );
if (payer != null ) {
logger . info ( "Payment succeeded: payer={}, path={}" ,
payer, request . getRequestURI ());
} else {
logger . warn ( "Payment required: path={}, ip={}" ,
request . getRequestURI (), request . getRemoteAddr ());
}
// Process request...
}
}
Examples
AI Agent API
@ WebServlet ( "/api/ai/chat" )
public class AIChatServlet extends HttpServlet {
@ Override
protected void doPost ( HttpServletRequest request ,
HttpServletResponse response )
throws ServletException , IOException {
String payer = (String) request . getAttribute ( "prism_payer" );
// Parse request body
BufferedReader reader = request . getReader ();
String message = new JSONObject ( reader . readLine ())
. getString ( "message" );
// Call AI service
String aiResponse = callAIService (message);
// Return response
response . setContentType ( "application/json" );
response . getWriter (). write (
new JSONObject ()
. put ( "response" , aiResponse)
. put ( "payer" , payer)
. put ( "creditsUsed" , 1 )
. toString ()
);
}
}
REST API with JAX-RS
import javax.ws.rs. * ;
import javax.ws.rs.core. * ;
@ Path ( "/api" )
public class ApiResource {
@ Context
private HttpServletRequest request ;
@ GET
@ Path ( "/premium" )
@ Produces ( MediaType . APPLICATION_JSON )
public Response premium () {
String payer = (String) request . getAttribute ( "prism_payer" );
return Response . ok ()
. entity ( Map . of (
"message" , "Premium content" ,
"payer" , payer
))
. build ();
}
}
Troubleshooting
Check filter mapping in web.xml: < filter-mapping >
< filter-name >PrismFilter</ filter-name >
< url-pattern >/api/*</ url-pattern > <!-- Must match your routes -->
</ filter-mapping >
Verify filter order: Prism filter should execute BEFORE authentication filters.
prism_payer attribute is null
Debug with logging: @ Override
protected void doGet ( HttpServletRequest request , ... ) {
System . out . println ( "URI: " + request . getRequestURI ());
System . out . println ( "Payment header: " + request . getHeader ( "X-PAYMENT" ));
System . out . println ( "Payer: " + request . getAttribute ( "prism_payer" ));
}
Check: Route matches filter URL pattern, payment header present and valid.
Common causes: 1. Insufficient balance 2. Invalid signature 3. Nonce reuse
4. Network timeout Check logs: Enable debug mode in PrismConfig.
Response wrapper causes issues
If you see encoding problems:
Make sure to set Content-Type before writing
Use getWriter() for text, getOutputStream() for binary
Don’t mix getWriter() and getOutputStream()