Blog • Artigo
    JEVBanking

    Jev in Practice: Intelligent Screening of Credit Card Service

    As Jev judges, confidently, what to do with a banking support call, and why the final decision remains with its code.

    Alexsander
    AlexsanderEngenheiro de Software
    Sep 27, 2026
    9 min de leitura
    Jev in Practice: Intelligent Screening of Credit Card Service

    Every bank with credit card operations faces the same operational bottleneck: the customer opens a ticket (the chip is not read, the charge appeared duplicated, the customer disputes a transaction) and someone, or something, needs to quickly decide what to do. This volume does not scale with human analysts. And resolving this by throwing the entire ticket to a generic AI, asking for a ready-made business decision in free text, is reckless: you would be putting a probabilistic model in charge of making a financial decision with regulatory effects, without control, without audit, and without predictability.

    This is exactly the problem that Jev, TypeSafe's System One model used here as a layer of structured judgment, was designed to solve. Jev does not return a ready-made decision in free text: it returns typed answers (noul, choice, score) to specific and measurable questions that I formulate about a ticket, leaving the final decision to the deterministic code of my application. This separation, Jev judges, the code decides, is the central point of everything that follows.

    All the code examples below are real, not hypothetical: they come from a demonstration repository that integrates Jev via TypeSafe's TypeScript SDK (@typesafe-ai/sdk), applied to a fictional scenario: the credit card customer service of Banco Aurora, with a card of two categories (Aurora Black and Aurora Classic). Each snippet can be copied and runs for real with npm run scenario <file-path>.

    Why Jev judges but does not decide

    A common mistake when integrating Jev is trying to use it as if it were a box that receives a ticket and returns a ready-made business decision ("issue a duplicate", "deny", "escalate"), throwing away exactly the guarantee that the noul/choice/score types provide. Leaving this business decision to an open-ended response, instead of composing several typed questions to Jev, has three serious problems in a banking context:

    First, the decision becomes opaque. If an auditor from the Central Bank asks why a refund was automatically denied, "Jev thought so" is not an acceptable answer; but "Jev returned judgment X with confidence 0.95, and rule Y requires 0.9" is.

    Second, the business logic gets trapped within the prompt. Every time the risk policy changes (and it changes frequently in banking compliance), you need to rewrite and revalidate an entire prompt, instead of changing a rule in versioned and testable code.

    Third, you lose the ability to compose signals. A real customer service decision combines Jev's judgment with data that the system already possesses: card category, issuance time, number of previous tickets, whether the card has already been blocked due to suspicion. If I ask Jev to decide everything at once, in a single question, I cannot mix these signals with weight and transparency later.

    The correct way to use Jev inverts this instinct: it does not decide, it judges specific propositions, and the code decides based on these judgments combined with the business rules.

    Part 1: State Preparation

    Before asking anything to Jev, I need to decide how to represent the ticket. This step is often underestimated, but it is where most judgment errors arise, because a poorly fed Jev produces a poorly grounded judgment, no matter how good the question is.

    Simple string versus structured object

    For a simple case, a string with the customer's report may suffice: this is what 1-state/01-ticket-card-defect/001-running-text.ts demonstrates. But a real banking ticket involves multiple interrelated fields: card issuance data, source channel, plan benefits, messages exchanged with customer service. In this scenario, a structured object is the right choice, for the same reasons that apply to any API contract: semantic clarity, testability, and direct persistence in the database. This is the difference between the two files in the 1-state/01-ticket-card-defect folder:

    Código
    const state = {
      ticket: {
        subject: "Card not read at the terminal",
        status: "open",
        channel: "app",
      },
      customer: {
        name: "Marina",
        card: "Aurora Black",
        card_benefit:
          "Free duplicate due to defect, within 12 months of issuance.",
      },
      issuance: {
        product: "Aurora Black Credit Card",
        annual_fee: 490,
        issued: "3 weeks ago",
      },
      message:
        "Hi, I got my Aurora Black card about three weeks ago and it stopped being read at the terminal. " +
        "The chip light doesn't even turn on, and I've tested it on two different terminals. Can you help me?",
    };
    
    const { answers } = await client.systemOne({
      state,
      questions: {
        cardDefect: noul("Does the `message` describe a defective card?"),
        coveredByBenefit: noul("Is the `issuance.product` covered for a free duplicate by the `customer.card_benefit`?"),
      },
    });
    

    Structuring the conversation as an ordered array

    When the ticket involved an exchange of messages with customer service, for example, a call, I represent this as an ordered array, not as a single concatenated text. 1-state/02-call-with-refund-request/003-speech-by-speech.ts takes this to the extreme, marking each speech with who said it:

    Código
    const state = {
      call_transcript: [
        { speaker: "agent", text: "Aurora Central, this is Sofia. How can I help?" },
        { speaker: "customer", text: "Hi. Actually, there are two things. First, I moved last month, I need to update my address." },
        // ...
        { speaker: "agent", text: "So I have two options. I can send a duplicate to your new address, or refund the annual fee in full." },
        { speaker: "customer", text: "What would you do?" },
        // ...
      ],
    };
    

    This preserves the chronology and the alternation between interlocutors, which is crucial for Jev to understand, for example, if the customer changed the version of the report during the call, a potentially useful signal of inconsistency for fraud investigation, not an isolated fraud judgment. Real fraud is decided with a much larger set of evidence (device, transaction history, establishment, speed, location, authentication, chargeback history); Jev contributes a contextual signal to this set, never the conclusion.

    Pre-calculation in code, never in the prompt

    Any value that the code can already calculate should arrive ready to Jev. I do not delegate to Jev operations that are deterministic: it was designed to judge unstructured content, not to do arithmetic. In 1-state/03-card-warranty-coverage/002-calculated-in-code.ts, the coverage of the benefit depends on how many months have passed since the card was issued, and this calculation is done in pure TypeScript, not described in natural language for Jev to resolve:

    Código
    function monthsBetween(from: string, to: string) {
      const start = new Date(from);
      const end = new Date(to);
      return (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth());
    }
    
    const months = monthsBetween(issuedOn, "2026-09-26");
    
    const { answers } = await client.systemOne({
      state: {
        customer: {
          name: "Marina Alves",
          card: "Aurora Black",
          card_benefit: "Free duplicate due to defect, within 12 months of issuance.",
        },
        issuance: { product: "Aurora Black Credit Card", issued: `${months} months ago` },
        message: "My card stopped being read at the terminal. Can you issue a duplicate?",
      },
      questions: {
        coveredByBenefit: noul("Is the `issuance.product` still covered by the `customer.card_benefit`?"),
      },
    });
    

    This reduces the error space of the judgment and lowers token costs, because I do not need to describe the calculation rule in natural language every time I call Jev.

    Avoiding noise and unnecessary data

    1-state/04-frustration-in-ticket-history/002-only-current-ticket.ts sends only the current ticket of the customer for Jev to judge frustration, not the complete history of service (this is the purpose of the file 001-complete-history.ts, for comparison). Besides the token cost, irrelevant information disperses Jev's judgment. I send only what is functionally necessary for the questions I will ask in the next step. This also has a direct implication for LGPD: the less personal data I inject into the state sent to Jev, the smaller the exposure surface, and this should be treated as a non-functional requirement, not as an optional good practice.

    Part 2: Formulating Judgment Questions

    This is the central part of how I use Jev. Instead of an open question like "is this ticket serious?", I break the judgment down into typed questions (the three forms that Jev understands: noul, choice, and score), each with a restricted form of response that the code can consume without ambiguity.

    choice: when there is exactly one exclusive answer

    I use choice when the options are mutually exclusive and I need exactly one answer. In 2-questions/04-judgment-criteria/004-structured-criteria.ts, each option carries a structured criterion (what it covers, what it explicitly does not cover, and examples) instead of just a phrase:

    Código
    team: choice("Which team should handle the `message`?", {
      billing: {
        covers: "Billing, invoice, transaction dispute, or refund",
        not_for: "Sending a card back or requesting a duplicate",
        examples: ["I was charged twice", "When will my refund arrive?"],
      },
      issuance: {
        covers: "Card issuance, delivery, duplicate, or shipping status",
        not_for: "The status of a refund or a charge",
        examples: ["Where's my card?", "How do I request a duplicate?"],
      },
      access: { /* ... */ },
      operation: { /* ... */ },
    }),
    

    This criterion is not decoration. It is where I explicitly define for Jev what each category means in my business domain, including what it does not cover. The comparison with 003-simple-criteria.ts, which uses only one phrase per option, shows how this structure reduces ambiguity between similar teams. Without this detailing, Jev applies its own intuition about where to draw the line, which may not coincide with the actual organization of the bank.

    noul: when multiple conditions can be true at the same time

    I use independent noul questions when the conditions are not mutually exclusive. A ticket can simultaneously request a supervisor and mention a complaint to the Central Bank. 2-questions/03-escalation-signals/002-separate-signals.ts breaks down the escalation judgment into four atomic signals:

    Código
    questions: {
      requestsSupervisor: noul("Does the `message` request to speak with a supervisor or someone more senior?"),
      threatensToCancel: noul("Does the `message` threaten to cancel the card or stop being a customer?"),
      mentionsProconOrJudicial: noul("Does the `message` mention legal action, Procon, or a complaint to the Central Bank (BACEN)?"),
      repeatedContact: noul("Does the `message` say that the customer has contacted about this before?"),
    },
    

    It is worth noting a specific detail of the banking domain here: in generic e-commerce customer service, a classic escalation signal is "the customer disputed the charge with their bank." This does not make sense in my scenario because I am the bank: there is no third party for the customer

    Este conteúdo foi útil?
    Compartilhar artigo

    Quer aplicar isso no seu contexto?

    Vamos conversar sobre seus desafios e encontrar o melhor caminho para sua operação.

    Agendar conversa