Build a Smart Credit Card Validator with Quarkus, Langchain4j, and Ollama
Validate cards like a bank, talk like a human.
Combining real validation logic with AI-powered conversational capabilities doesn’t require cloud APIs, third-party vendors, or PCI compliance headaches. In this hands-on tutorial, you’ll build a smart credit card validator using Quarkus, Langchain4j, and a local Ollama LLM running on your machine. This app will not only validate card numbers using the Luhn algorithm and brand prefixes, but also simulate realistic CVC validation using HMAC.
You’ll guide a local LLM to interpret natural language inputs and orchestrate tools in your Java application. Think: “Is this card okay to use?” becomes a real query your app can answer.
Prerequisites
Java 17+
Maven 3.9+
Quarkus CLI (
brew install quarkusio/tap/quarkus
or SDKMAN)Ollama locally installed (https://ollama.com) or as Dev Service
A model like
llama3.1
(with tool calling capabilities)
Create Your Quarkus Project
You can follow along as usual, or just grab the complete example from my Github repository.
quarkus create app com.example:credit-card-validator \
--extensions='rest-jackson,langchain4j-ollama'
cd credit-card-validator
Update your application.properties
for local Ollama:
quarkus.langchain4j.ollama.chat-model.model-id=llama3.1:latest
quarkus.langchain4j.ollama.timeout=60s
# Development & Debugging Configuration
quarkus.langchain4j.ollama.log-requests=true
quarkus.langchain4j.ollama.log-responses=true
quarkus.langchain4j.ollama.devservices.enabled=false
#Card Security Example
app.secret.hmac-key=a-very-super-secret-and-long-key-for-hmac-calculation
Implement Validation Logic
First, we will build the core service without any AI. This service will expose a simple REST endpoint to validate a credit card number and detect its brand.
Luhn Check (Checksum Algorithm)
The Luhn algorithm (or Mod-10 algorithm) is a checksum formula used to validate most credit card numbers. We'll create a class to encapsulate this logic.
Create a new file: src/main/java/com/example/LuhnAlgorithm.java
package com.example;
public class LuhnAlgorithm {
public static boolean isValid(String cardNumber) {
if (cardNumber == null || cardNumber.isBlank())
return false;
String cleaned = cardNumber.replaceAll("[\\s-]+", "");
int sum = 0;
boolean alt = false;
for (int i = cleaned.length() - 1; i >= 0; i--) {
int n = Integer.parseInt(cleaned.substring(i, i + 1));
if (alt) {
n *= 2;
if (n > 9)
n = (n % 10) + 1;
}
sum += n;
alt = !alt;
}
return (sum % 10 == 0);
}
}
Card Brand Detection
Credit card brands are identified by their unique prefixes. We'll create a simple detector using if-else
logic.
Create a new file: src/main/java/com/example/CardBrandDetector.java
package com.example;
public class CardBrandDetector {
public enum Brand {
VISA, MASTERCARD, AMERICAN_EXPRESS, DISCOVER, UNKNOWN
}
public static Brand detect(String cardNumber) {
if (cardNumber == null || cardNumber.isBlank())
return Brand.UNKNOWN;
String cleaned = cardNumber.replaceAll("[\\s-]+", "");
if (cleaned.startsWith("4"))
return Brand.VISA;
if (cleaned.matches("^5[1-5].*"))
return Brand.MASTERCARD;
if (cleaned.startsWith("34") || cleaned.startsWith("37"))
return Brand.AMERICAN_EXPRESS;
if (cleaned.startsWith("6011") || cleaned.startsWith("65"))
return Brand.DISCOVER;
return Brand.UNKNOWN;
}
}
REST API for Validation
Now, let's expose our validation logic via a REST API.
Create the file src/main/java/com/example/CreditCardResource.java
(you can rename the existing GreetingResource
).
package com.example;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path("/validate")
public class CreditCardResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public ValidationResult validate(@QueryParam("cardNumber") String cardNumber) {
boolean valid = LuhnAlgorithm.isValid(cardNumber);
String brand = CardBrandDetector.detect(cardNumber).name();
return new ValidationResult(valid, brand);
}
public record ValidationResult(boolean valid, String brand) {
}
}
Start dev mode:
quarkus dev
Test it:
curl "http://localhost:8080/validate?cardNumber=49927398716"
You should see the following JSON response, confirming our basic logic works:
{"valid":true,"brand":"VISA"}
The AI Upgrade - Introducing Langchain4j
Now for the exciting part. We will empower our service with conversational AI. Instead of a rigid API, we'll allow users to make requests in natural language.
The magic of Langchain4j agents lies in "tools." We will refactor our validation logic into a dedicated class and annotate its methods with @Tool
. This exposes them to the AI, which can then decide when to use them.
Create src/main/java/com/example/CreditCardTools.java
:
package com.example;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import dev.langchain4j.agent.tool.Tool;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CreditCardTools {
// Inject the secret key for HMAC
@ConfigProperty(name = "app.secret.hmac-key")
String hmacSecretKey;
private enum Brand {
VISA, MASTERCARD, AMERICAN_EXPRESS, DISCOVER, UNKNOWN
}
private Brand detect(String cardNumber) {
// ... (implementation from Part 1, Step 3)
if (cardNumber == null || cardNumber.replaceAll("\\s+", "").isEmpty()) {
return Brand.UNKNOWN;
}
String cleanCardNumber = cardNumber.replaceAll("\\s+", "");
if (cleanCardNumber.startsWith("4")) {
return Brand.VISA;
} else if (cleanCardNumber.matches("^5[1-5].*")) {
return Brand.MASTERCARD;
} else if (cleanCardNumber.startsWith("34") || cleanCardNumber.startsWith("37")) {
return Brand.AMERICAN_EXPRESS;
} else if (cleanCardNumber.startsWith("6011") || cleanCardNumber.startsWith("65")) {
return Brand.DISCOVER;
}
return Brand.UNKNOWN;
}
private boolean isLuhnValid(String cardNumber) {
// ... (implementation from Part 1, Step 2)
if (cardNumber == null || cardNumber.replaceAll("\\s+", "").isEmpty()) {
return false;
}
String cleanCardNumber = cardNumber.replaceAll("\\s+", "");
int sum = 0;
boolean alternate = false;
for (int i = cleanCardNumber.length() - 1; i >= 0; i--) {
int n = Integer.parseInt(cleanCardNumber.substring(i, i + 1));
if (alternate) {
n *= 2;
if (n > 9) {
n = (n % 10) + 1;
}
}
sum += n;
alternate = !alternate;
}
return (sum % 10 == 0);
}
@Tool("Validates a credit card number using the Luhn algorithm and determines its brand.")
public String validateCreditCard(String creditCardNumber) {
String cleanedNumber = creditCardNumber.replaceAll("[\\s-]+", "");
boolean isValid = isLuhnValid(cleanedNumber);
if (!isValid) {
return "The provided credit card number '" + creditCardNumber
+ "' is invalid according to the Luhn algorithm.";
}
Brand brand = detect(cleanedNumber);
return "The credit card number '" + creditCardNumber + "' is valid. The brand is " + brand + ".";
}
/**
* Calculates the expected CVC and validates it along with the expiration date.
* The creditCardNumber is required to calculate the expected CVC.
* The expiration date must be in MM/YY or MM/YYYY format.
*/
@Tool("Checks if a credit card is expired and if its CVC is correct. Requires the full credit card number to validate the CVC.")
public String validateCvcAndExpiration(String creditCardNumber, String cvc, String expirationDate) {
// 1. Calculate the expected CVC
String expectedCvc = calculateExpectedCvc(creditCardNumber, hmacSecretKey);
// 2. CVC Check
if (!expectedCvc.equals(cvc)) {
return "The provided CVC is incorrect.";
}
// 3. Expiration Date Check
try {
DateTimeFormatter formatter = expirationDate.length() == 5 ? DateTimeFormatter.ofPattern("MM/yy")
: DateTimeFormatter.ofPattern("MM/yyyy");
YearMonth expiry = YearMonth.parse(expirationDate, formatter);
// The card is valid through the end of its expiration month.
if (expiry.isBefore(YearMonth.now())) {
return "The card is expired.";
}
} catch (DateTimeParseException e) {
return "Invalid expiration date format. Please use MM/YY or MM/YYYY.";
}
// 4. Success
return "The CVC is correct and the card is not expired.";
}
private String calculateExpectedCvc(String cardNumber, String secretKey) {
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key_spec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256_HMAC.init(secret_key_spec);
byte[] hmac_hash = sha256_HMAC
.doFinal(cardNumber.replaceAll("[\\s-]+", "").getBytes(StandardCharsets.UTF_8));
int hash_code = Arrays.hashCode(hmac_hash);
int cvc_value = Math.abs(hash_code % 1000);
return String.format("%03d", cvc_value);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error calculating CVC", e);
}
}
}
Key points:
@ApplicationScoped
: This makes our tools class a manageable bean.@Tool
: This annotation, along with its descriptive text, is what Langchain4j uses to understand what a method does and when it should be called. Clear descriptions are super important.
We are simulating CVC validation by dynamically calculating an expected CVC from the card number and a secret key. This is a far more realistic pattern than using a static CVC.
AI Orchestration via LangChain4j
Next, define an AI service interface. This is the contract between your application and the LLM.
Create src/main/java/com/example/CreditCardAiService.java
:
package com.example;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
@RegisterAiService(tools = CreditCardTools.class)
public interface CreditCardAiService {
@SystemMessage("""
You are a helpful assistant for validating credit card details.
You have access to two tools:
1. A tool to validate the credit card NUMBER and find its brand.
2. A tool to check the card's CVC and expiration date. To use this tool, you MUST provide it with the credit card number, the CVC, and the expiration date.
When a user asks to validate their card, you must gather the card number, the expiration date, and the CVC from their query.
You must call BOTH tools to perform a full validation.
When calling the CVC validation tool, you must pass the card number to it.
Finally, combine the results from both tools into a single, comprehensive summary for the user.
If any part of the validation fails, clearly state the reason for the failure.
""")
String chat(@UserMessage String userMessage);
}
What this class does:
@RegisterAiService(tools = ...)
: This is the important link. It tells Langchain4j that this AI service is empowered with all@Tool
methods found in theCreditCardTools
class.@SystemMessage
: This provides the initial instructions to the AI, defining its persona and primary goal. This detailed prompt is important. It trains the AI to act as an orchestrator, understanding that a full validation requires data from both tools.
AI Chat REST Endpoint
Finally, let's create a new, much simpler REST endpoint that uses our AI service.
Modify src/main/java/com/example/CreditCardResource.java
:
@Inject
CreditCardAiService aiService;
@POST
@Path("/withAi")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public String chatWithValidator(String message) {
return aiService.chat(message);
}
Run Ollama and Test It
Start Ollama locally (or just Quarkus in Dev Mode) :
ollama run llama3.1
quarkus dev
Let's find the correct CVC for a test card. Our calculateExpectedCvc
function is deterministic; for the card number 49927398716
and the secret key we defined, the calculated CVC will be 777
.
Send an AI query:
curl -X POST -H "Content-Type: text/plain" \
-d "Hey, can you check my card 49927398716 with CVC 133 and expiry 12/28?" \
http://localhost:8080/withAi
And see the result:
The credit card number '49927398716' is valid. The brand is VISA.
The CVC is correct and the card is not expired.
Your card details are valid! Everything checks out: the card number, the expiration date, and the CVC all match what we have on file. Your VISA card is good to go!
Try invalid cases to see how it reacts.
How Real CVC Checks Work
Our HMAC example is a great conceptual model, but it's important to understand how this works in the real world.
Proprietary Algorithms: Card networks like Visa (CVV2) and Mastercard (CVC2) use proprietary, secret cryptographic algorithms. These are not public knowledge.
Hardware Security Modules (HSMs): This validation logic is performed inside highly secure, tamper-resistant hardware devices called HSMs, which are managed by the card-issuing banks. The secret cryptographic keys used for validation never leave the HSM.
Card-Specific Keys (CVKs): The core of the system involves one or more secret keys known as CVKs (Card Verification Keys). For a given card, the bank's HSM uses the correct CVK, along with other data like the Primary Account Number (PAN) and expiration date, to generate the valid CVC value.
The Validation Flow: When you make an online purchase, the CVC you enter is encrypted and sent through the payment gateway to the issuing bank. The bank's HSM performs the same calculation using its stored CVK. If the value you entered matches the value it just calculated, the CVC is considered valid.
No Storage by Merchants: A critical rule of the Payment Card Industry Data Security Standard (PCI DSS) is that merchants are strictly prohibited from storing the CVC after a transaction is authorized. This is a key security feature.
Our tutorial's approach correctly simulates the most important concept: the CVC is not a random number but is deterministically derived from card information and a secret key known only to the issuer.
You Just Built a Local AI Validator
No cloud APIs. No third-party AI keys. Just pure Java, Quarkus, and a local LLM using Ollama. Your AI agent can now interpret freeform input, orchestrate multiple validation steps, and return friendly, useful responses—all in milliseconds.
Next steps? You could add logging, metrics, or even secure the endpoints with API keys and rate limiting.
But for now: you’ve got a smart credit card agent running in your terminal. Bravo.
interesting one Markus :)) Learnt something new today