diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml
index e8b835f7400..5b498edfd40 100644
--- a/extra/bundle/pom.xml
+++ b/extra/bundle/pom.xml
@@ -70,6 +70,11 @@
live-intent-omni-channel-identity
${project.version}
+
+ org.prebid.server.hooks.modules
+ pb-rule-engine
+ ${project.version}
+
diff --git a/extra/modules/pb-rule-engine/pom.xml b/extra/modules/pb-rule-engine/pom.xml
new file mode 100644
index 00000000000..1903ee73045
--- /dev/null
+++ b/extra/modules/pb-rule-engine/pom.xml
@@ -0,0 +1,15 @@
+
+
+ 4.0.0
+
+
+ org.prebid.server.hooks.modules
+ all-modules
+ 3.33.0-SNAPSHOT
+
+
+ pb-rule-engine
+
+ pb-rule-engine
+ Rule engine module
+
diff --git a/extra/modules/pb-rule-engine/src/lombok.config b/extra/modules/pb-rule-engine/src/lombok.config
new file mode 100644
index 00000000000..efd92714219
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/lombok.config
@@ -0,0 +1 @@
+lombok.anyConstructor.addConstructorProperties = true
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java
new file mode 100644
index 00000000000..2caf6ee8b30
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java
@@ -0,0 +1,76 @@
+package org.prebid.server.hooks.modules.rule.engine.config;
+
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Vertx;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.modules.rule.engine.core.config.AccountConfigParser;
+import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser;
+import org.prebid.server.hooks.modules.rule.engine.core.config.StageConfigParser;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestConditionalRuleFactory;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestStageSpecification;
+import org.prebid.server.hooks.modules.rule.engine.v1.PbRuleEngineModule;
+import org.prebid.server.json.ObjectMapperProvider;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Clock;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.random.RandomGenerator;
+
+@Configuration
+@ConditionalOnProperty(prefix = "hooks." + PbRuleEngineModule.CODE, name = "enabled", havingValue = "true")
+public class PbRuleEngineModuleConfiguration {
+
+ @Bean
+ PbRuleEngineModule ruleEngineModule(RuleParser ruleParser,
+ @Value("${datacenter-region:#{null}}") String datacenter) {
+
+ return new PbRuleEngineModule(ruleParser, datacenter);
+ }
+
+ @Bean
+ StageConfigParser processedAuctionRequestStageParser(
+ BidderCatalog bidderCatalog) {
+
+ final RandomGenerator randomGenerator = () -> ThreadLocalRandom.current().nextLong();
+
+ return new StageConfigParser<>(
+ randomGenerator,
+ Stage.processed_auction_request,
+ new RequestStageSpecification(ObjectMapperProvider.mapper(), bidderCatalog, randomGenerator),
+ new RequestConditionalRuleFactory());
+ }
+
+ @Bean
+ AccountConfigParser accountConfigParser(
+ StageConfigParser processedAuctionRequestStageParser) {
+
+ return new AccountConfigParser(ObjectMapperProvider.mapper(), processedAuctionRequestStageParser);
+ }
+
+ @Bean
+ RuleParser ruleParser(
+ @Value("${hooks.pb-rule-engine.rule-cache.expire-after-minutes}") long cacheExpireAfterMinutes,
+ @Value("${hooks.pb-rule-engine.rule-cache.max-size}") long cacheMaxSize,
+ @Value("${hooks.pb-rule-engine.rule-parsing.retry-initial-delay-millis}") long delay,
+ @Value("${hooks.pb-rule-engine.rule-parsing.retry-max-delay-millis}") long maxDelay,
+ @Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-factor}") double factor,
+ @Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-jitter}") double jitter,
+ AccountConfigParser accountConfigParser,
+ Vertx vertx,
+ Clock clock) {
+
+ return new RuleParser(
+ cacheExpireAfterMinutes,
+ cacheMaxSize,
+ ExponentialBackoffRetryPolicy.of(delay, maxDelay, factor, jitter),
+ accountConfigParser,
+ vertx,
+ clock);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java
new file mode 100644
index 00000000000..6f542881c66
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java
@@ -0,0 +1,48 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule;
+
+import java.util.Objects;
+
+public class AccountConfigParser {
+
+ private final ObjectMapper mapper;
+ private final StageConfigParser processedAuctionRequestStageParser;
+
+ public AccountConfigParser(
+ ObjectMapper mapper,
+ StageConfigParser processedAuctionRequestStageParser) {
+
+ this.mapper = Objects.requireNonNull(mapper);
+ this.processedAuctionRequestStageParser = Objects.requireNonNull(processedAuctionRequestStageParser);
+ }
+
+ public PerStageRule parse(ObjectNode accountConfig) {
+ final AccountConfig parsedConfig;
+ try {
+ parsedConfig = mapper.treeToValue(accountConfig, AccountConfig.class);
+ } catch (JsonProcessingException e) {
+ throw new PreBidException(e.getMessage());
+ }
+
+ if (!parsedConfig.isEnabled()) {
+ return PerStageRule.builder()
+ .timestamp(parsedConfig.getTimestamp())
+ .processedAuctionRequestRule(NoOpRule.create())
+ .build();
+ }
+
+ return PerStageRule.builder()
+ .timestamp(parsedConfig.getTimestamp())
+ .processedAuctionRequestRule(processedAuctionRequestStageParser.parse(parsedConfig))
+ .build();
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java
new file mode 100644
index 00000000000..3136a0cefca
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java
@@ -0,0 +1,152 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.apache.commons.lang3.ObjectUtils;
+import org.prebid.server.execution.retry.RetryPolicy;
+import org.prebid.server.execution.retry.Retryable;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+public class RuleParser {
+
+ private static final Logger logger = LoggerFactory.getLogger(RuleParser.class);
+
+ private final AccountConfigParser parser;
+ private final Vertx vertx;
+ private final Clock clock;
+
+ private final RetryPolicy retryPolicy;
+
+ private final Map accountIdToParsingAttempt;
+ private final Map accountIdToRules;
+
+ public RuleParser(long cacheExpireAfterMinutes,
+ long cacheMaxSize,
+ RetryPolicy retryPolicy,
+ AccountConfigParser parser,
+ Vertx vertx,
+ Clock clock) {
+
+ this.parser = Objects.requireNonNull(parser);
+ this.vertx = Objects.requireNonNull(vertx);
+ this.clock = Objects.requireNonNull(clock);
+ this.retryPolicy = Objects.requireNonNull(retryPolicy);
+
+ this.accountIdToParsingAttempt = Caffeine.newBuilder()
+ .expireAfterAccess(cacheExpireAfterMinutes, TimeUnit.MINUTES)
+ .maximumSize(cacheMaxSize)
+ .build()
+ .asMap();
+
+ this.accountIdToRules = Caffeine.newBuilder()
+ .expireAfterAccess(cacheExpireAfterMinutes, TimeUnit.MINUTES)
+ .maximumSize(cacheMaxSize)
+ .build()
+ .asMap();
+ }
+
+ public Future parseForAccount(String accountId, ObjectNode config) {
+ final PerStageRule cachedRule = accountIdToRules.get(accountId);
+
+ if (cachedRule != null && cachedRule.timestamp().compareTo(getConfigTimestamp(config)) >= 0) {
+ return Future.succeededFuture(cachedRule);
+ }
+
+ parseConfig(accountId, config);
+ return Future.succeededFuture(ObjectUtils.defaultIfNull(cachedRule, PerStageRule.noOp()));
+ }
+
+ private Instant getConfigTimestamp(ObjectNode config) {
+ try {
+ return Optional.of(config)
+ .map(node -> node.get("timestamp"))
+ .filter(JsonNode::isTextual)
+ .map(JsonNode::asText)
+ .map(Instant::parse)
+ .orElse(Instant.EPOCH);
+ } catch (DateTimeParseException exception) {
+ return Instant.EPOCH;
+ }
+ }
+
+ private void parseConfig(String accountId, ObjectNode config) {
+ final Instant now = clock.instant();
+ final ParsingAttempt attempt = accountIdToParsingAttempt.compute(
+ accountId, (ignored, previousAttempt) -> tryRegisteringNewAttempt(previousAttempt, now));
+
+ // reference equality used on purpose - if references are equal - then we should parse
+ if (attempt.timestamp() == now) {
+ logger.info("Parsing rule for account {}", accountId);
+ vertx.executeBlocking(() -> parser.parse(config))
+ .onSuccess(result -> succeedParsingAttempt(accountId, result))
+ .onFailure(error -> failParsingAttempt(accountId, attempt, error));
+ }
+ }
+
+ private ParsingAttempt tryRegisteringNewAttempt(ParsingAttempt previousAttempt, Instant currentAttemptStart) {
+ if (previousAttempt == null) {
+ return new ParsingAttempt.InProgress(currentAttemptStart, retryPolicy);
+ }
+
+ if (previousAttempt instanceof ParsingAttempt.InProgress) {
+ return previousAttempt;
+ }
+
+ if (previousAttempt.retryPolicy() instanceof Retryable previousAttemptRetryPolicy) {
+ final Instant previouslyDecidedToRetryAfter = previousAttempt.timestamp().plus(
+ Duration.ofMillis(previousAttemptRetryPolicy.delay()));
+
+ return previouslyDecidedToRetryAfter.isBefore(currentAttemptStart)
+ ? new ParsingAttempt.InProgress(currentAttemptStart, previousAttemptRetryPolicy.next())
+ : previousAttempt;
+ }
+
+ return previousAttempt;
+ }
+
+ private void succeedParsingAttempt(String accountId, PerStageRule result) {
+ accountIdToRules.put(accountId, result);
+ accountIdToParsingAttempt.remove(accountId);
+
+ logger.debug("Successfully parsed rule-engine config for account {}", accountId);
+ }
+
+ private void failParsingAttempt(String accountId, ParsingAttempt attempt, Throwable cause) {
+ accountIdToParsingAttempt.put(accountId, ((ParsingAttempt.InProgress) attempt).failed());
+
+ logger.error(
+ "Failed to parse rule-engine config for account %s: %s".formatted(accountId, cause.getMessage()),
+ cause);
+ }
+
+ private sealed interface ParsingAttempt {
+
+ Instant timestamp();
+
+ RetryPolicy retryPolicy();
+
+ record Failed(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt {
+ }
+
+ record InProgress(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt {
+
+ public Failed failed() {
+ return new Failed(timestamp, retryPolicy);
+ }
+ }
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java
new file mode 100644
index 00000000000..12c2d9a2720
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java
@@ -0,0 +1,175 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config;
+
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountRuleConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.config.model.ModelGroupConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.config.model.ResultFunctionConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.config.model.RuleSetConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.config.model.SchemaFunctionConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.AlternativeActionRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.CompositeRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.DefaultActionRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RandomWeightedRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidMatcherConfiguration;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTreeFactory;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException;
+import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedEntry;
+import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.random.RandomGenerator;
+
+public class StageConfigParser {
+
+ private final RandomGenerator randomGenerator;
+ private final StageSpecification specification;
+ private final Stage stage;
+ private final ConditionalRuleFactory conditionalRuleFactory;
+
+ public StageConfigParser(RandomGenerator randomGenerator,
+ Stage stage,
+ StageSpecification specification,
+ ConditionalRuleFactory conditionalRuleFactory) {
+
+ this.randomGenerator = Objects.requireNonNull(randomGenerator);
+ this.stage = Objects.requireNonNull(stage);
+ this.specification = Objects.requireNonNull(specification);
+ this.conditionalRuleFactory = Objects.requireNonNull(conditionalRuleFactory);
+ }
+
+ public Rule parse(AccountConfig config) {
+ final List> stageSubrules = config.getRuleSets().stream()
+ .filter(ruleSet -> stage.equals(ruleSet.getStage()))
+ .filter(RuleSetConfig::isEnabled)
+ .map(RuleSetConfig::getModelGroups)
+ .map(this::parseModelGroupConfigs)
+ .toList();
+
+ return stageSubrules.isEmpty()
+ ? NoOpRule.create()
+ : CompositeRule.of(stageSubrules);
+ }
+
+ private Rule parseModelGroupConfigs(List modelGroupConfigs) {
+ final List>> weightedRules = modelGroupConfigs.stream()
+ .map(config -> WeightedEntry.of(config.getWeight(), parseModelGroupConfig(config)))
+ .toList();
+
+ return RandomWeightedRule.of(randomGenerator, new WeightedList<>(weightedRules));
+ }
+
+ private Rule parseModelGroupConfig(ModelGroupConfig config) {
+ final Rule matchingRule = parseMatchingRule(config);
+ final Rule defaultRule = parseDefaultActionRule(config);
+
+ return combineRules(matchingRule, defaultRule);
+ }
+
+ private Rule parseMatchingRule(ModelGroupConfig config) {
+ final List schemaConfig = config.getSchema();
+ final List rulesConfig = config.getRules();
+
+ if (CollectionUtils.isEmpty(schemaConfig) || CollectionUtils.isEmpty(rulesConfig)) {
+ return null;
+ }
+
+ final Schema schema = parseSchema(schemaConfig);
+
+ final List> rules = rulesConfig.stream()
+ .map(this::parseRuleConfig)
+ .toList();
+ final RuleTree> ruleTree = RuleTreeFactory.buildTree(rules);
+
+ if (schemaConfig.size() != ruleTree.getDepth()) {
+ throw new InvalidMatcherConfiguration("Schema functions count and rules matchers count mismatch");
+ }
+
+ return conditionalRuleFactory.create(schema, ruleTree, config.getAnalyticsKey(), config.getVersion());
+ }
+
+ private Schema parseSchema(List schema) {
+ final List> schemaFunctions = schema.stream()
+ .map(config -> SchemaFunctionHolder.of(
+ config.getFunction(),
+ specification.schemaFunctionByName(config.getFunction()),
+ config.getArgs()))
+ .toList();
+
+ schemaFunctions.forEach(this::validateFunctionConfig);
+
+ return Schema.of(schemaFunctions);
+ }
+
+ private void validateFunctionConfig(SchemaFunctionHolder holder) {
+ try {
+ holder.getSchemaFunction().validateConfig(holder.getConfig());
+ } catch (ConfigurationValidationException exception) {
+ throw new InvalidMatcherConfiguration(
+ "Function '%s' configuration is invalid: %s".formatted(holder.getName(), exception.getMessage()));
+ }
+ }
+
+ private RuleConfig parseRuleConfig(AccountRuleConfig ruleConfig) {
+ final String ruleFired = String.join("|", ruleConfig.getConditions());
+ final List> actions = parseActions(ruleConfig.getResults());
+
+ return RuleConfig.of(ruleFired, actions);
+ }
+
+ private Rule parseDefaultActionRule(ModelGroupConfig config) {
+ final List defaultActionConfig = config.getDefaultAction();
+
+ if (CollectionUtils.isEmpty(config.getDefaultAction())) {
+ return null;
+ }
+
+ return new DefaultActionRule<>(
+ parseActions(defaultActionConfig), config.getAnalyticsKey(), config.getVersion());
+ }
+
+ private List> parseActions(List functionConfigs) {
+ final List> actions = functionConfigs.stream()
+ .map(config -> ResultFunctionHolder.of(
+ config.getFunction(),
+ specification.resultFunctionByName(config.getFunction()),
+ config.getArgs()))
+ .toList();
+
+ actions.forEach(this::validateActionConfig);
+
+ return actions;
+ }
+
+ private void validateActionConfig(ResultFunctionHolder action) {
+ try {
+ action.getFunction().validateConfig(action.getConfig());
+ } catch (ConfigurationValidationException exception) {
+ throw new InvalidMatcherConfiguration(
+ "Function '%s' configuration is invalid: %s".formatted(action.getName(), exception.getMessage()));
+ }
+ }
+
+ private Rule combineRules(Rule left, Rule right) {
+ if (left == null && right == null) {
+ return NoOpRule.create();
+ } else if (left != null && right != null) {
+ return AlternativeActionRule.of(left, right);
+ } else if (left != null) {
+ return AlternativeActionRule.of(left, NoOpRule.create());
+ }
+
+ return AlternativeActionRule.of(right, NoOpRule.create());
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java
new file mode 100644
index 00000000000..fab0cd9f208
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java
@@ -0,0 +1,26 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Value;
+import lombok.extern.jackson.Jacksonized;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+
+@Value
+@Builder
+@Jacksonized
+public class AccountConfig {
+
+ @Builder.Default
+ boolean enabled = true;
+
+ @Builder.Default
+ Instant timestamp = Instant.EPOCH;
+
+ @Builder.Default
+ @JsonProperty("ruleSets")
+ List ruleSets = Collections.emptyList();
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java
new file mode 100644
index 00000000000..6feb59c8e66
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java
@@ -0,0 +1,13 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config.model;
+
+import lombok.Value;
+
+import java.util.List;
+
+@Value(staticConstructor = "of")
+public class AccountRuleConfig {
+
+ List conditions;
+
+ List results;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java
new file mode 100644
index 00000000000..f646de8985e
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java
@@ -0,0 +1,26 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Value;
+
+import java.util.List;
+
+@Value
+@Builder
+public class ModelGroupConfig {
+
+ int weight;
+
+ @JsonProperty("analyticsKey")
+ String analyticsKey;
+
+ String version;
+
+ List schema;
+
+ @JsonProperty("default")
+ List defaultAction;
+
+ List rules;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java
new file mode 100644
index 00000000000..2b642c633e2
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java
@@ -0,0 +1,12 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config.model;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class ResultFunctionConfig {
+
+ String function;
+
+ ObjectNode args;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java
new file mode 100644
index 00000000000..9c54f100977
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java
@@ -0,0 +1,27 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Value;
+import lombok.extern.jackson.Jacksonized;
+import org.prebid.server.hooks.execution.model.Stage;
+
+import java.util.List;
+
+@Value
+@Builder
+@Jacksonized
+public class RuleSetConfig {
+
+ @Builder.Default
+ boolean enabled = true;
+
+ Stage stage;
+
+ String name;
+
+ String version;
+
+ @JsonProperty("modelGroups")
+ List modelGroups;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java
new file mode 100644
index 00000000000..264abef359a
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java
@@ -0,0 +1,12 @@
+package org.prebid.server.hooks.modules.rule.engine.core.config.model;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class SchemaFunctionConfig {
+
+ String function;
+
+ ObjectNode args;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java
new file mode 100644
index 00000000000..e820c787a71
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java
@@ -0,0 +1,18 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request;
+
+public sealed interface Granularity {
+
+ final class Request implements Granularity {
+ private static final Request INSTANCE = new Request();
+
+ private Request() {
+ }
+
+ public static Request instance() {
+ return INSTANCE;
+ }
+ }
+
+ record Imp(String impId) implements Granularity {
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java
new file mode 100644
index 00000000000..5b644c8cde5
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java
@@ -0,0 +1,39 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult;
+
+import java.util.Objects;
+
+public class PerImpConditionalRule implements Rule {
+
+ private final ConditionalRule delegate;
+
+ public PerImpConditionalRule(ConditionalRule delegate) {
+ this.delegate = Objects.requireNonNull(delegate);
+ }
+
+ @Override
+ public RuleResult process(BidRequest value, RequestRuleContext context) {
+ RuleResult result = RuleResult.noAction(value);
+ for (Imp imp : value.getImp()) {
+ result = result.mergeWith(delegate.process(result.getValue(), contextForImp(context, imp)));
+
+ if (result.isReject()) {
+ return result;
+ }
+ }
+
+ return result;
+ }
+
+ private RequestRuleContext contextForImp(RequestRuleContext context, Imp imp) {
+ return RequestRuleContext.of(
+ context.getAuctionContext(),
+ new Granularity.Imp(imp.getId()),
+ context.getDatacenter());
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java
new file mode 100644
index 00000000000..7958d1e97b9
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java
@@ -0,0 +1,31 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request;
+
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree;
+
+public class RequestConditionalRuleFactory implements ConditionalRuleFactory {
+
+ @Override
+ public Rule create(
+ Schema schema,
+ RuleTree> ruleTree,
+ String analyticsKey,
+ String modelVersion) {
+
+ final ConditionalRule requestMatchingRule = new ConditionalRule<>(
+ schema, ruleTree, analyticsKey, modelVersion);
+
+ return schema.getFunctions().stream()
+ .map(SchemaFunctionHolder::getName)
+ .anyMatch(RequestStageSpecification.PER_IMP_SCHEMA_FUNCTIONS::contains)
+
+ ? new PerImpConditionalRule(requestMatchingRule)
+ : requestMatchingRule;
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java
new file mode 100644
index 00000000000..35ea4e1f6b0
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java
@@ -0,0 +1,14 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request;
+
+import lombok.Value;
+import org.prebid.server.auction.model.AuctionContext;
+
+@Value(staticConstructor = "of")
+public class RequestRuleContext {
+
+ AuctionContext auctionContext;
+
+ Granularity granularity;
+
+ String datacenter;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java
new file mode 100644
index 00000000000..953dd11a74b
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java
@@ -0,0 +1,103 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter.ExcludeBiddersFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter.IncludeBiddersFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log.LogATagFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.AdUnitCodeFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.AdUnitCodeInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.BundleFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.BundleInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.ChannelFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DataCenterFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DataCenterInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceCountryFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceCountryInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceTypeFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceTypeInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DomainFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DomainInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.EidAvailableFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.EidInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.FpdAvailableFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.GppSidAvailableFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.GppSidInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.MediaTypeInFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.PrebidKeyFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.TcfInScopeFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.UserFpdAvailableFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidResultFunctionException;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidSchemaFunctionException;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.functions.PercentFunction;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.random.RandomGenerator;
+
+public class RequestStageSpecification implements StageSpecification {
+
+ public static final Set PER_IMP_SCHEMA_FUNCTIONS =
+ Set.of(AdUnitCodeFunction.NAME, AdUnitCodeInFunction.NAME, MediaTypeInFunction.NAME);
+
+ private final Map> schemaFunctions;
+ private final Map> resultFunctions;
+
+ public RequestStageSpecification(ObjectMapper mapper,
+ BidderCatalog bidderCatalog,
+ RandomGenerator random) {
+
+ schemaFunctions = new HashMap<>();
+ schemaFunctions.put(AdUnitCodeFunction.NAME, new AdUnitCodeFunction());
+ schemaFunctions.put(AdUnitCodeInFunction.NAME, new AdUnitCodeInFunction());
+ schemaFunctions.put(BundleFunction.NAME, new BundleFunction());
+ schemaFunctions.put(BundleInFunction.NAME, new BundleInFunction());
+ schemaFunctions.put(ChannelFunction.NAME, new ChannelFunction());
+ schemaFunctions.put(DataCenterFunction.NAME, new DataCenterFunction());
+ schemaFunctions.put(DataCenterInFunction.NAME, new DataCenterInFunction());
+ schemaFunctions.put(DeviceCountryFunction.NAME, new DeviceCountryFunction());
+ schemaFunctions.put(DeviceCountryInFunction.NAME, new DeviceCountryInFunction());
+ schemaFunctions.put(DeviceTypeFunction.NAME, new DeviceTypeFunction());
+ schemaFunctions.put(DeviceTypeInFunction.NAME, new DeviceTypeInFunction());
+ schemaFunctions.put(DomainFunction.NAME, new DomainFunction());
+ schemaFunctions.put(DomainInFunction.NAME, new DomainInFunction());
+ schemaFunctions.put(EidAvailableFunction.NAME, new EidAvailableFunction());
+ schemaFunctions.put(EidInFunction.NAME, new EidInFunction());
+ schemaFunctions.put(FpdAvailableFunction.NAME, new FpdAvailableFunction());
+ schemaFunctions.put(GppSidAvailableFunction.NAME, new GppSidAvailableFunction());
+ schemaFunctions.put(GppSidInFunction.NAME, new GppSidInFunction());
+ schemaFunctions.put(MediaTypeInFunction.NAME, new MediaTypeInFunction());
+ schemaFunctions.put(PercentFunction.NAME, new PercentFunction<>(random));
+ schemaFunctions.put(PrebidKeyFunction.NAME, new PrebidKeyFunction());
+ schemaFunctions.put(TcfInScopeFunction.NAME, new TcfInScopeFunction());
+ schemaFunctions.put(UserFpdAvailableFunction.NAME, new UserFpdAvailableFunction());
+
+ resultFunctions = Map.of(
+ IncludeBiddersFunction.NAME, new IncludeBiddersFunction(mapper, bidderCatalog),
+ ExcludeBiddersFunction.NAME, new ExcludeBiddersFunction(mapper, bidderCatalog),
+ LogATagFunction.NAME, new LogATagFunction(mapper));
+ }
+
+ public SchemaFunction schemaFunctionByName(String name) {
+ final SchemaFunction function = schemaFunctions.get(name);
+ if (function == null) {
+ throw new InvalidSchemaFunctionException(name);
+ }
+
+ return function;
+ }
+
+ public ResultFunction resultFunctionByName(String name) {
+ final ResultFunction function = resultFunctions.get(name);
+ if (function == null) {
+ throw new InvalidResultFunctionException(name);
+ }
+
+ return function;
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java
new file mode 100644
index 00000000000..0f20384d401
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java
@@ -0,0 +1,85 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.auction.model.BidRejectionReason;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments;
+import org.prebid.server.hooks.v1.analytics.Result;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid;
+import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+public class AnalyticsMapper {
+
+ private static final String ACTIVITY_NAME = "pb-rule-engine";
+ private static final String SUCCESS_STATUS = "success";
+
+ private AnalyticsMapper() {
+ }
+
+ public static Tags toTags(ObjectMapper mapper,
+ String functionName,
+ List seatNonBids,
+ InfrastructureArguments infrastructureArguments,
+ String analyticsValue) {
+
+ final String analyticsKey = infrastructureArguments.getAnalyticsKey();
+ if (StringUtils.isEmpty(analyticsKey)) {
+ return TagsImpl.of(Collections.emptyList());
+ }
+
+ final List removedBidders = seatNonBids.stream()
+ .map(SeatNonBid::getSeat)
+ .distinct()
+ .toList();
+ if (CollectionUtils.isEmpty(removedBidders)) {
+ return TagsImpl.of(Collections.emptyList());
+ }
+
+ final List impIds = seatNonBids.stream()
+ .flatMap(seatNonBid -> seatNonBid.getNonBid().stream())
+ .map(NonBid::getImpId)
+ .distinct()
+ .toList();
+
+ final BidRejectionReason reason = seatNonBids.stream()
+ .flatMap(seatNonBid -> seatNonBid.getNonBid().stream())
+ .map(NonBid::getStatusCode)
+ .findAny()
+ .orElse(null);
+
+ final AnalyticsData analyticsData = new AnalyticsData(
+ analyticsKey,
+ analyticsValue,
+ infrastructureArguments.getModelVersion(),
+ infrastructureArguments.getRuleFired(),
+ functionName,
+ removedBidders,
+ reason);
+
+ final Result result = ResultImpl.of(
+ SUCCESS_STATUS, mapper.valueToTree(analyticsData), AppliedToImpl.builder().impIds(impIds).build());
+
+ return TagsImpl.of(Collections.singletonList(
+ ActivityImpl.of(ACTIVITY_NAME, SUCCESS_STATUS, Collections.singletonList(result))));
+ }
+
+ private record AnalyticsData(@JsonProperty("analyticsKey") String analyticsKey,
+ @JsonProperty("analyticsValue") String analyticsValue,
+ @JsonProperty("modelVersion") String modelVersion,
+ @JsonProperty("conditionFired") String conditionFired,
+ @JsonProperty("resultFunction") String resultFunction,
+ @JsonProperty("biddersRemoved") List biddersRemoved,
+ @JsonProperty("seatNonBid") BidRejectionReason seatNonBid) {
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java
new file mode 100644
index 00000000000..b954abd664a
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java
@@ -0,0 +1,36 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.Imp;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.cookie.UidsCookie;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ExcludeBiddersFunction extends FilterBiddersFunction {
+
+ public static final String NAME = "excludeBidders";
+
+ public ExcludeBiddersFunction(ObjectMapper objectMapper, BidderCatalog bidderCatalog) {
+ super(objectMapper, bidderCatalog);
+ }
+
+ @Override
+ protected Set biddersToRemove(Imp imp, Boolean ifSyncedId, Set bidders, UidsCookie uidsCookie) {
+ final ObjectNode biddersNode = FilterUtils.bidderNode(imp.getExt());
+
+ return StreamUtil.asStream(biddersNode.fieldNames())
+ .filter(bidder -> FilterUtils.containsIgnoreCase(bidders.stream(), bidder))
+ .filter(bidder ->
+ ifSyncedId == null || ifSyncedId == isBidderIdSynced(bidder.toLowerCase(), uidsCookie))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ protected String name() {
+ return NAME;
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java
new file mode 100644
index 00000000000..8e2887c4ed8
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java
@@ -0,0 +1,148 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.auction.model.BidRejectionReason;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.cookie.UidsCookie;
+import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid;
+import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public abstract class FilterBiddersFunction implements ResultFunction {
+
+ private final ObjectMapper mapper;
+ protected final BidderCatalog bidderCatalog;
+
+ public FilterBiddersFunction(ObjectMapper mapper, BidderCatalog bidderCatalog) {
+ this.mapper = Objects.requireNonNull(mapper);
+ this.bidderCatalog = Objects.requireNonNull(bidderCatalog);
+ }
+
+ @Override
+ public RuleResult apply(ResultFunctionArguments arguments) {
+ final FilterBiddersFunctionConfig config = parseConfig(arguments.getConfig());
+
+ final BidRequest bidRequest = arguments.getOperand();
+ final InfrastructureArguments infrastructureArguments =
+ arguments.getInfrastructureArguments();
+
+ final UidsCookie uidsCookie = infrastructureArguments.getContext().getAuctionContext().getUidsCookie();
+ final Boolean ifSyncedId = config.getIfSyncedId();
+ final BidRejectionReason rejectionReason = config.getSeatNonBid();
+ final Granularity granularity = infrastructureArguments.getContext().getGranularity();
+
+ final List updatedImps = new ArrayList<>();
+ final List seatNonBid = new ArrayList<>();
+
+ for (Imp imp : bidRequest.getImp()) {
+ if (granularity instanceof Granularity.Imp(String impId) && !StringUtils.equals(impId, imp.getId())) {
+ updatedImps.add(imp);
+ continue;
+ }
+
+ switch (filterBidders(imp, config.getBidders(), ifSyncedId, uidsCookie)) {
+ case FilterBiddersResult.NoAction noAction -> updatedImps.add(imp);
+
+ case FilterBiddersResult.Reject reject ->
+ seatNonBid.addAll(toSeatNonBid(imp.getId(), reject.bidders(), rejectionReason));
+
+ case FilterBiddersResult.Update update -> {
+ updatedImps.add(update.imp());
+ seatNonBid.addAll(toSeatNonBid(imp.getId(), update.bidders(), rejectionReason));
+ }
+ }
+ }
+
+ final Tags tags = AnalyticsMapper.toTags(
+ mapper, name(), seatNonBid, infrastructureArguments, config.getAnalyticsValue());
+
+ if (updatedImps.isEmpty()) {
+ return RuleResult.rejected(tags, seatNonBid);
+ }
+
+ final RuleAction action = !seatNonBid.isEmpty() ? RuleAction.UPDATE : RuleAction.NO_ACTION;
+ final BidRequest result = action == RuleAction.UPDATE
+ ? bidRequest.toBuilder().imp(updatedImps).build()
+ : bidRequest;
+
+ return RuleResult.of(result, action, tags, seatNonBid);
+ }
+
+ private static List toSeatNonBid(String impId, Set bidders, BidRejectionReason reason) {
+ return bidders.stream()
+ .map(bidder -> SeatNonBid.of(bidder, Collections.singletonList(NonBid.of(impId, reason))))
+ .toList();
+ }
+
+ private FilterBiddersResult filterBidders(Imp imp,
+ Set bidders,
+ Boolean ifSyncedId,
+ UidsCookie uidsCookie) {
+
+ final Set biddersToRemove = biddersToRemove(imp, ifSyncedId, bidders, uidsCookie);
+ if (biddersToRemove.isEmpty()) {
+ return FilterBiddersResult.NoAction.instance();
+ }
+
+ final ObjectNode updatedExt = imp.getExt().deepCopy();
+ final ObjectNode updatedBiddersNode = FilterUtils.bidderNode(updatedExt);
+ biddersToRemove.forEach(updatedBiddersNode::remove);
+
+ return updatedBiddersNode.isEmpty()
+ ? new FilterBiddersResult.Reject(biddersToRemove)
+ : new FilterBiddersResult.Update(imp.toBuilder().ext(updatedExt).build(), biddersToRemove);
+ }
+
+ protected abstract Set biddersToRemove(Imp imp,
+ Boolean ifSyncedId,
+ Set bidders,
+ UidsCookie uidsCookie);
+
+ protected boolean isBidderIdSynced(String bidder, UidsCookie uidsCookie) {
+ return bidderCatalog.cookieFamilyName(bidder)
+ .map(uidsCookie::hasLiveUidFrom)
+ .orElse(false);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ final FilterBiddersFunctionConfig parsedConfig = parseConfig(config);
+ if (parsedConfig == null) {
+ throw new ConfigurationValidationException("Configuration is required, but not provided");
+ }
+
+ if (CollectionUtils.isEmpty(parsedConfig.getBidders())) {
+ throw new ConfigurationValidationException("'bidders' field is required");
+ }
+ }
+
+ private FilterBiddersFunctionConfig parseConfig(ObjectNode config) {
+ try {
+ return mapper.treeToValue(config, FilterBiddersFunctionConfig.class);
+ } catch (JsonProcessingException e) {
+ throw new ConfigurationValidationException(e.getMessage());
+ }
+ }
+
+ protected abstract String name();
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java
new file mode 100644
index 00000000000..28ba0f5e518
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java
@@ -0,0 +1,27 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Value;
+import lombok.extern.jackson.Jacksonized;
+import org.prebid.server.auction.model.BidRejectionReason;
+
+import java.util.Set;
+
+@Value
+@Builder
+@Jacksonized
+public class FilterBiddersFunctionConfig {
+
+ Set bidders;
+
+ @Builder.Default
+ @JsonProperty("seatnonbid")
+ BidRejectionReason seatNonBid = BidRejectionReason.REQUEST_BLOCKED_OPTIMIZED;
+
+ @JsonProperty("ifSyncedId")
+ Boolean ifSyncedId;
+
+ @JsonProperty("analyticsValue")
+ String analyticsValue;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java
new file mode 100644
index 00000000000..48f71682f84
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java
@@ -0,0 +1,22 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter;
+
+import com.iab.openrtb.request.Imp;
+
+import java.util.Set;
+
+public sealed interface FilterBiddersResult {
+
+ record NoAction() implements FilterBiddersResult {
+ private static final NoAction INSTANCE = new NoAction();
+
+ public static NoAction instance() {
+ return INSTANCE;
+ }
+ }
+
+ record Update(Imp imp, Set bidders) implements FilterBiddersResult {
+ }
+
+ record Reject(Set bidders) implements FilterBiddersResult {
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java
new file mode 100644
index 00000000000..d3355c04bd7
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java
@@ -0,0 +1,30 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class FilterUtils {
+
+ private static final String PREBID = "prebid";
+ private static final String BIDDER = "bidder";
+
+ private FilterUtils() {
+ }
+
+ public static ObjectNode bidderNode(ObjectNode impExt) {
+ return Optional.ofNullable(impExt.get(PREBID))
+ .filter(JsonNode::isObject)
+ .map(prebidNode -> (ObjectNode) prebidNode)
+ .map(prebidNode -> prebidNode.get(BIDDER))
+ .filter(JsonNode::isObject)
+ .map(bidderNode -> (ObjectNode) bidderNode)
+ .orElseThrow(() -> new IllegalStateException("Impression without ext.prebid.bidder"));
+ }
+
+ public static boolean containsIgnoreCase(Stream stream, String value) {
+ return stream.anyMatch(value::equalsIgnoreCase);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java
new file mode 100644
index 00000000000..08ea8f2b223
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java
@@ -0,0 +1,35 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.Imp;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.cookie.UidsCookie;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class IncludeBiddersFunction extends FilterBiddersFunction {
+
+ public static final String NAME = "includeBidders";
+
+ public IncludeBiddersFunction(ObjectMapper objectMapper, BidderCatalog bidderCatalog) {
+ super(objectMapper, bidderCatalog);
+ }
+
+ @Override
+ protected Set biddersToRemove(Imp imp, Boolean ifSyncedId, Set bidders, UidsCookie uidsCookie) {
+ final ObjectNode biddersNode = FilterUtils.bidderNode(imp.getExt());
+
+ return StreamUtil.asStream(biddersNode.fieldNames())
+ .filter(bidder -> !FilterUtils.containsIgnoreCase(bidders.stream(), bidder)
+ || (ifSyncedId != null && ifSyncedId != isBidderIdSynced(bidder.toLowerCase(), uidsCookie)))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ protected String name() {
+ return NAME;
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java
new file mode 100644
index 00000000000..68c78e65d53
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java
@@ -0,0 +1,61 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments;
+import org.prebid.server.hooks.v1.analytics.Result;
+import org.prebid.server.hooks.v1.analytics.Tags;
+
+import java.util.Collections;
+import java.util.List;
+
+public class AnalyticsMapper {
+
+ private static final String ACTIVITY_NAME = "pb-rule-engine";
+ private static final String SUCCESS_STATUS = "success";
+
+ private AnalyticsMapper() {
+ }
+
+ public static Tags toTags(ObjectMapper mapper,
+ InfrastructureArguments infrastructureArguments,
+ String analyticsValue) {
+
+ final String analyticsKey = infrastructureArguments.getAnalyticsKey();
+ if (StringUtils.isEmpty(analyticsKey)) {
+ return TagsImpl.of(Collections.emptyList());
+ }
+
+ final AnalyticsData analyticsData = new AnalyticsData(
+ analyticsKey,
+ analyticsValue,
+ infrastructureArguments.getModelVersion(),
+ infrastructureArguments.getRuleFired(),
+ LogATagFunction.NAME);
+
+ final Granularity granularity = infrastructureArguments.getContext().getGranularity();
+ final List impIds = granularity instanceof Granularity.Imp(String impId)
+ ? Collections.singletonList(impId)
+ : Collections.singletonList("*");
+
+ final Result result = ResultImpl.of(
+ SUCCESS_STATUS, mapper.valueToTree(analyticsData), AppliedToImpl.builder().impIds(impIds).build());
+
+ return TagsImpl.of(Collections.singletonList(
+ ActivityImpl.of(ACTIVITY_NAME, SUCCESS_STATUS, Collections.singletonList(result))));
+ }
+
+ private record AnalyticsData(@JsonProperty("analyticsKey") String analyticsKey,
+ @JsonProperty("analyticsValue") String analyticsValue,
+ @JsonProperty("modelVersion") String modelVersion,
+ @JsonProperty("conditionFired") String conditionFired,
+ @JsonProperty("resultFunction") String resultFunction) {
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java
new file mode 100644
index 00000000000..fe7b7543302
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java
@@ -0,0 +1,43 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.hooks.v1.analytics.Tags;
+
+import java.util.Collections;
+import java.util.Objects;
+
+public class LogATagFunction implements ResultFunction {
+
+ public static final String NAME = "logAtag";
+
+ private static final String ANALYTICS_VALUE_FIELD = "analyticsValue";
+
+ private final ObjectMapper mapper;
+
+ public LogATagFunction(ObjectMapper mapper) {
+ this.mapper = Objects.requireNonNull(mapper);
+ }
+
+ @Override
+ public RuleResult apply(ResultFunctionArguments arguments) {
+ final Tags tags = AnalyticsMapper.toTags(
+ mapper,
+ arguments.getInfrastructureArguments(),
+ arguments.getConfig().get(ANALYTICS_VALUE_FIELD).asText());
+
+ return RuleResult.of(arguments.getOperand(), RuleAction.NO_ACTION, tags, Collections.emptyList());
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertString(config, ANALYTICS_VALUE_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java
new file mode 100644
index 00000000000..165293f8b71
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java
@@ -0,0 +1,35 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.apache.commons.collections4.ListUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.AdUnitCodeUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+public class AdUnitCodeFunction implements SchemaFunction {
+
+ public static final String NAME = "adUnitCode";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final RequestRuleContext context = arguments.getContext();
+ final String impId = ((Granularity.Imp) context.getGranularity()).impId();
+ final BidRequest bidRequest = arguments.getOperand();
+
+ return ListUtils.emptyIfNull(bidRequest.getImp()).stream()
+ .filter(imp -> StringUtils.equals(imp.getId(), impId))
+ .findFirst()
+ .flatMap(AdUnitCodeUtils::extractAdUnitCode)
+ .orElse(UNDEFINED_RESULT);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java
new file mode 100644
index 00000000000..6b015863f97
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java
@@ -0,0 +1,60 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.apache.commons.collections4.ListUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.AdUnitCodeUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class AdUnitCodeInFunction implements SchemaFunction {
+
+ public static final String NAME = "adUnitCodeIn";
+
+ private static final String CODES_FIELD = "codes";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final RequestRuleContext context = arguments.getContext();
+ final String impId = ((Granularity.Imp) context.getGranularity()).impId();
+ final BidRequest bidRequest = arguments.getOperand();
+
+ final Imp adUnit = ListUtils.emptyIfNull(bidRequest.getImp()).stream()
+ .filter(imp -> StringUtils.equals(imp.getId(), impId))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException(
+ "Critical error in rules engine. Imp id of absent imp supplied"));
+
+ final Set adUnitPotentialCodes = Stream.of(
+ AdUnitCodeUtils.extractGpid(adUnit),
+ AdUnitCodeUtils.extractTagId(adUnit),
+ AdUnitCodeUtils.extractPbAdSlot(adUnit),
+ AdUnitCodeUtils.extractStoredRequestId(adUnit))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(CODES_FIELD).elements())
+ .map(JsonNode::asText)
+ .anyMatch(adUnitPotentialCodes::contains);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfStrings(config, CODES_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java
new file mode 100644
index 00000000000..c99f0b618a0
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java
@@ -0,0 +1,28 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+import java.util.Optional;
+
+public class BundleFunction implements SchemaFunction {
+
+ public static final String NAME = "bundle";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ return Optional.ofNullable(arguments.getOperand().getApp())
+ .map(App::getBundle)
+ .orElse(UNDEFINED_RESULT);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java
new file mode 100644
index 00000000000..9389d686938
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java
@@ -0,0 +1,38 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Optional;
+
+public class BundleInFunction implements SchemaFunction {
+
+ public static final String NAME = "bundleIn";
+
+ private static final String BUNDLES_FIELD = "bundles";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final String bundle = Optional.ofNullable(arguments.getOperand().getApp())
+ .map(App::getBundle)
+ .orElse(UNDEFINED_RESULT);
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(BUNDLES_FIELD).elements())
+ .map(JsonNode::asText)
+ .anyMatch(bundle::equals);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfStrings(config, BUNDLES_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java
new file mode 100644
index 00000000000..2c8a5fd94aa
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java
@@ -0,0 +1,38 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
+
+import java.util.Optional;
+
+public class ChannelFunction implements SchemaFunction {
+
+ public static final String NAME = "channel";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ return Optional.of(arguments.getOperand())
+ .map(BidRequest::getExt)
+ .map(ExtRequest::getPrebid)
+ .map(ExtRequestPrebid::getChannel)
+ .map(ExtRequestPrebidChannel::getName)
+ .map(ChannelFunction::resolveChannel)
+ .orElse(SchemaFunction.UNDEFINED_RESULT);
+ }
+
+ private static String resolveChannel(String channel) {
+ return channel.equals("pbjs") ? "web" : channel;
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java
new file mode 100644
index 00000000000..6d3e7acc7c1
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java
@@ -0,0 +1,24 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+public class DataCenterFunction implements SchemaFunction {
+
+ public static final String NAME = "dataCenter";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ return StringUtils.defaultIfEmpty(arguments.getContext().getDatacenter(), UNDEFINED_RESULT);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java
new file mode 100644
index 00000000000..8679d9c70e3
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java
@@ -0,0 +1,34 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+public class DataCenterInFunction implements SchemaFunction {
+
+ public static final String NAME = "dataCenterIn";
+
+ private static final String DATACENTERS_FIELD = "datacenters";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final String datacenter = StringUtils.defaultIfEmpty(arguments.getContext().getDatacenter(), UNDEFINED_RESULT);
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(DATACENTERS_FIELD).elements())
+ .map(JsonNode::asText)
+ .anyMatch(datacenter::equalsIgnoreCase);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfStrings(config, DATACENTERS_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java
new file mode 100644
index 00000000000..f2444f58985
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java
@@ -0,0 +1,31 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Geo;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+import java.util.Optional;
+
+public class DeviceCountryFunction implements SchemaFunction {
+
+ public static final String NAME = "deviceCountry";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ return Optional.of(arguments.getOperand())
+ .map(BidRequest::getDevice)
+ .map(Device::getGeo)
+ .map(Geo::getCountry)
+ .orElse(UNDEFINED_RESULT);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java
new file mode 100644
index 00000000000..d2dbf5904ef
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java
@@ -0,0 +1,40 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Geo;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Optional;
+
+public class DeviceCountryInFunction implements SchemaFunction {
+
+ public static final String NAME = "deviceCountryIn";
+ private static final String COUNTRIES_FIELD = "countries";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final String deviceCountry = Optional.of(arguments.getOperand())
+ .map(BidRequest::getDevice)
+ .map(Device::getGeo)
+ .map(Geo::getCountry)
+ .orElse(UNDEFINED_RESULT);
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(COUNTRIES_FIELD).elements())
+ .map(JsonNode::asText)
+ .anyMatch(deviceCountry::equalsIgnoreCase);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfStrings(config, COUNTRIES_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java
new file mode 100644
index 00000000000..490db16a351
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java
@@ -0,0 +1,29 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+import java.util.Optional;
+
+public class DeviceTypeFunction implements SchemaFunction {
+
+ public static final String NAME = "deviceType";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ return Optional.ofNullable(arguments.getOperand().getDevice())
+ .map(Device::getDevicetype)
+ .map(String::valueOf)
+ .orElse(UNDEFINED_RESULT);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java
new file mode 100644
index 00000000000..daacb7ad434
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java
@@ -0,0 +1,42 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Optional;
+
+public class DeviceTypeInFunction implements SchemaFunction {
+
+ public static final String NAME = "deviceTypeIn";
+
+ public static final String TYPES_FIELD = "types";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final Integer deviceType = Optional.ofNullable(arguments.getOperand().getDevice())
+ .map(Device::getDevicetype)
+ .orElse(null);
+
+ if (deviceType == null) {
+ return Boolean.FALSE.toString();
+ }
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(TYPES_FIELD).elements())
+ .map(JsonNode::asInt)
+ .anyMatch(deviceType::equals);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfIntegers(config, TYPES_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java
new file mode 100644
index 00000000000..0fb1e39082f
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java
@@ -0,0 +1,25 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.DomainUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+public class DomainFunction implements SchemaFunction {
+
+ public static final String NAME = "domain";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ return DomainUtils.extractDomain(arguments.getOperand())
+ .orElse(UNDEFINED_RESULT);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java
new file mode 100644
index 00000000000..bfd07a13ea9
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java
@@ -0,0 +1,50 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.DomainUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class DomainInFunction implements SchemaFunction {
+
+ public static final String NAME = "domainIn";
+
+ private static final String DOMAINS_FIELD = "domains";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final BidRequest bidRequest = arguments.getOperand();
+
+ final Set suppliedDomains = Stream.of(
+ DomainUtils.extractSitePublisherDomain(bidRequest),
+ DomainUtils.extractAppPublisherDomain(bidRequest),
+ DomainUtils.extractDoohPublisherDomain(bidRequest),
+ DomainUtils.extractSiteDomain(bidRequest),
+ DomainUtils.extractAppDomain(bidRequest),
+ DomainUtils.extractDoohDomain(bidRequest))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(DOMAINS_FIELD).elements())
+ .map(JsonNode::asText)
+ .anyMatch(suppliedDomains::contains);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfStrings(config, DOMAINS_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java
new file mode 100644
index 00000000000..9c9c8fb2a99
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java
@@ -0,0 +1,33 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.User;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+import java.util.Optional;
+
+public class EidAvailableFunction implements SchemaFunction {
+
+ public static final String NAME = "eidAvailable";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final boolean available = Optional.of(arguments.getOperand())
+ .map(BidRequest::getUser)
+ .map(User::getEids)
+ .filter(ListUtil::isNotEmpty)
+ .isPresent();
+
+ return Boolean.toString(available);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java
new file mode 100644
index 00000000000..6c9e4d0070f
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java
@@ -0,0 +1,46 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Eid;
+import com.iab.openrtb.request.User;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class EidInFunction implements SchemaFunction {
+
+ public static final String NAME = "eidIn";
+
+ private static final String SOURCES_FIELD = "sources";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final Set sources = Optional.of(arguments.getOperand())
+ .map(BidRequest::getUser)
+ .map(User::getEids)
+ .stream()
+ .flatMap(Collection::stream)
+ .map(Eid::getSource)
+ .collect(Collectors.toSet());
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(SOURCES_FIELD).elements())
+ .map(JsonNode::asText)
+ .anyMatch(sources::contains);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfStrings(config, SOURCES_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java
new file mode 100644
index 00000000000..66c19e14b36
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java
@@ -0,0 +1,90 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Content;
+import com.iab.openrtb.request.Site;
+import com.iab.openrtb.request.User;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.proto.openrtb.ext.request.ExtApp;
+import org.prebid.server.proto.openrtb.ext.request.ExtSite;
+import org.prebid.server.proto.openrtb.ext.request.ExtUser;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+public class FpdAvailableFunction implements SchemaFunction {
+
+ public static final String NAME = "fpdAvailable";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final BidRequest bidRequest = arguments.getOperand();
+
+ final boolean available = isUserDataAvailable(bidRequest)
+ || isUserExtDataAvailable(bidRequest)
+ || isSiteContentDataAvailable(bidRequest)
+ || isSiteExtDataAvailable(bidRequest)
+ || isAppContentDataAvailable(bidRequest)
+ || isAppExtDataAvailable(bidRequest);
+
+ return Boolean.toString(available);
+ }
+
+ private static boolean isUserDataAvailable(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getUser())
+ .map(User::getData)
+ .filter(ListUtil::isNotEmpty)
+ .isPresent();
+ }
+
+ private static boolean isUserExtDataAvailable(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getUser())
+ .map(User::getExt)
+ .map(ExtUser::getData)
+ .filter(Predicate.not(ObjectNode::isEmpty))
+ .isPresent();
+ }
+
+ private static boolean isSiteContentDataAvailable(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getSite())
+ .map(Site::getContent)
+ .map(Content::getData)
+ .filter(ListUtil::isNotEmpty)
+ .isPresent();
+ }
+
+ private static boolean isSiteExtDataAvailable(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getSite())
+ .map(Site::getExt)
+ .map(ExtSite::getData)
+ .filter(Predicate.not(ObjectNode::isEmpty))
+ .isPresent();
+ }
+
+ private static boolean isAppContentDataAvailable(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getApp())
+ .map(App::getContent)
+ .map(Content::getData)
+ .filter(ListUtil::isNotEmpty)
+ .isPresent();
+ }
+
+ private static boolean isAppExtDataAvailable(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getApp())
+ .map(App::getExt)
+ .map(ExtApp::getData)
+ .filter(Predicate.not(ObjectNode::isEmpty))
+ .isPresent();
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java
new file mode 100644
index 00000000000..ef306e65351
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java
@@ -0,0 +1,36 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Regs;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Optional;
+
+public class GppSidAvailableFunction implements SchemaFunction {
+
+ public static final String NAME = "gppSidAvailable";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final boolean available = Optional.of(arguments.getOperand())
+ .map(BidRequest::getRegs)
+ .map(Regs::getGppSid)
+ .stream()
+ .flatMap(Collection::stream)
+ .filter(Objects::nonNull)
+ .anyMatch(sid -> sid > 0);
+
+ return Boolean.toString(available);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java
new file mode 100644
index 00000000000..46bf0aad29c
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java
@@ -0,0 +1,44 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Regs;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class GppSidInFunction implements SchemaFunction {
+
+ public static final String NAME = "gppSidIn";
+
+ private static final String SIDS_FIELD = "sids";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final Set sids = Optional.of(arguments.getOperand())
+ .map(BidRequest::getRegs)
+ .map(Regs::getGppSid)
+ .stream()
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+
+ final boolean matches = StreamUtil.asStream(arguments.getConfig().get(SIDS_FIELD).elements())
+ .map(JsonNode::asInt)
+ .anyMatch(sids::contains);
+
+ return Boolean.toString(matches);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfIntegers(config, SIDS_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java
new file mode 100644
index 00000000000..547efb749ee
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java
@@ -0,0 +1,70 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.apache.commons.collections4.ListUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.spring.config.bidder.model.MediaType;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class MediaTypeInFunction implements SchemaFunction {
+
+ public static final String NAME = "mediaTypeIn";
+
+ private static final String TYPES_FIELD = "types";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final RequestRuleContext context = arguments.getContext();
+ final String impId = ((Granularity.Imp) context.getGranularity()).impId();
+ final BidRequest bidRequest = arguments.getOperand();
+
+ final Imp adUnit = ListUtils.emptyIfNull(bidRequest.getImp()).stream()
+ .filter(imp -> StringUtils.equals(imp.getId(), impId))
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException(
+ "Critical error in rules engine. Imp id of absent imp supplied"));
+
+ final Set adUnitMediaTypes = adUnitMediaTypes(adUnit);
+
+ final boolean intersects = StreamUtil.asStream(arguments.getConfig().get(TYPES_FIELD).elements())
+ .map(JsonNode::asText)
+ .anyMatch(adUnitMediaTypes::contains);
+
+ return Boolean.toString(intersects);
+ }
+
+ private static Set adUnitMediaTypes(Imp imp) {
+ final Set result = new HashSet<>();
+
+ if (imp.getBanner() != null) {
+ result.add(MediaType.BANNER.getKey());
+ }
+ if (imp.getVideo() != null) {
+ result.add(MediaType.VIDEO.getKey());
+ }
+ if (imp.getAudio() != null) {
+ result.add(MediaType.AUDIO.getKey());
+ }
+ if (imp.getXNative() != null) {
+ result.add(MediaType.NATIVE.getKey());
+ }
+
+ return result;
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertArrayOfStrings(config, TYPES_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java
new file mode 100644
index 00000000000..17c42beb5b5
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java
@@ -0,0 +1,36 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+
+import java.util.Optional;
+
+public class PrebidKeyFunction implements SchemaFunction {
+
+ public static final String NAME = "prebidKey";
+
+ private static final String KEY_FIELD = "key";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ return Optional.ofNullable(arguments.getOperand().getExt())
+ .map(ExtRequest::getPrebid)
+ .map(ExtRequestPrebid::getKvps)
+ .map(kvps -> kvps.get(arguments.getConfig().get(KEY_FIELD).asText()))
+ .filter(JsonNode::isTextual)
+ .map(JsonNode::asText)
+ .orElse(UNDEFINED_RESULT);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertString(config, KEY_FIELD);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java
new file mode 100644
index 00000000000..b98d9672a44
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java
@@ -0,0 +1,32 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Regs;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+
+import java.util.Optional;
+
+public class TcfInScopeFunction implements SchemaFunction {
+
+ public static final String NAME = "tcfInScope";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final boolean inScope = Optional.of(arguments.getOperand())
+ .map(BidRequest::getRegs)
+ .map(Regs::getGdpr)
+ .filter(Integer.valueOf(1)::equals)
+ .isPresent();
+
+ return Boolean.toString(inScope);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java
new file mode 100644
index 00000000000..2a4ecc83206
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java
@@ -0,0 +1,41 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.User;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil;
+import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils;
+import org.prebid.server.proto.openrtb.ext.request.ExtUser;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+public class UserFpdAvailableFunction implements SchemaFunction {
+
+ public static final String NAME = "userFpdAvailable";
+
+ @Override
+ public String extract(SchemaFunctionArguments arguments) {
+ final Optional user = Optional.of(arguments.getOperand())
+ .map(BidRequest::getUser);
+
+ final boolean userDataAvailable = user.map(User::getData)
+ .filter(ListUtil::isNotEmpty)
+ .isPresent();
+
+ final boolean userExtDataAvailable = user.map(User::getExt)
+ .map(ExtUser::getData)
+ .filter(Predicate.not(ObjectNode::isEmpty))
+ .isPresent();
+
+ return Boolean.toString(userDataAvailable || userExtDataAvailable);
+ }
+
+ @Override
+ public void validateConfig(ObjectNode config) {
+ ValidationUtils.assertNoArgs(config);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java
new file mode 100644
index 00000000000..1d7c8093fa6
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java
@@ -0,0 +1,49 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.iab.openrtb.request.Imp;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Optional;
+
+public class AdUnitCodeUtils {
+
+ private AdUnitCodeUtils() {
+ }
+
+ public static Optional extractAdUnitCode(Imp imp) {
+ return extractGpid(imp)
+ .or(() -> extractTagId(imp))
+ .or(() -> extractPbAdSlot(imp))
+ .or(() -> extractStoredRequestId(imp));
+ }
+
+ public static Optional extractGpid(Imp imp) {
+ return Optional.ofNullable(imp.getExt())
+ .map(ext -> ext.get("gpid"))
+ .filter(JsonNode::isTextual)
+ .map(JsonNode::asText);
+ }
+
+ public static Optional extractTagId(Imp imp) {
+ return Optional.ofNullable(imp.getTagid())
+ .filter(StringUtils::isNotBlank);
+ }
+
+ public static Optional extractPbAdSlot(Imp imp) {
+ return Optional.ofNullable(imp.getExt())
+ .map(ext -> ext.get("data"))
+ .map(data -> data.get("pbadslot"))
+ .filter(JsonNode::isTextual)
+ .map(JsonNode::asText);
+ }
+
+ public static Optional extractStoredRequestId(Imp imp) {
+ return Optional.ofNullable(imp.getExt())
+ .map(ext -> ext.get("prebid"))
+ .map(prebid -> prebid.get("storedrequest"))
+ .map(storedRequest -> storedRequest.get("id"))
+ .filter(JsonNode::isTextual)
+ .map(JsonNode::asText);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java
new file mode 100644
index 00000000000..b9d57ef8aed
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java
@@ -0,0 +1,62 @@
+package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util;
+
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Dooh;
+import com.iab.openrtb.request.Publisher;
+import com.iab.openrtb.request.Site;
+
+import java.util.Optional;
+
+public class DomainUtils {
+
+ private DomainUtils() {
+ }
+
+ public static Optional extractDomain(BidRequest bidRequest) {
+ return extractPublisherDomain(bidRequest)
+ .or(() -> extractPlainDomain(bidRequest));
+ }
+
+ private static Optional extractPublisherDomain(BidRequest bidRequest) {
+ return extractSitePublisherDomain(bidRequest)
+ .or(() -> extractAppPublisherDomain(bidRequest))
+ .or(() -> extractDoohPublisherDomain(bidRequest));
+ }
+
+ public static Optional extractSitePublisherDomain(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getSite())
+ .map(Site::getPublisher)
+ .map(Publisher::getDomain);
+ }
+
+ public static Optional extractAppPublisherDomain(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getApp())
+ .map(App::getPublisher)
+ .map(Publisher::getDomain);
+ }
+
+ public static Optional extractDoohPublisherDomain(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getDooh())
+ .map(Dooh::getPublisher)
+ .map(Publisher::getDomain);
+ }
+
+ private static Optional extractPlainDomain(BidRequest bidRequest) {
+ return extractSiteDomain(bidRequest)
+ .or(() -> extractAppDomain(bidRequest))
+ .or(() -> extractDoohDomain(bidRequest));
+ }
+
+ public static Optional extractSiteDomain(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getSite()).map(Site::getDomain);
+ }
+
+ public static Optional extractAppDomain(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getApp()).map(App::getDomain);
+ }
+
+ public static Optional extractDoohDomain(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getDooh()).map(Dooh::getDomain);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java
new file mode 100644
index 00000000000..4370ef58502
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java
@@ -0,0 +1,19 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import lombok.Value;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException;
+
+@Value(staticConstructor = "of")
+public class AlternativeActionRule implements Rule {
+
+ Rule delegate;
+ Rule alternative;
+
+ public RuleResult process(T value, C context) {
+ try {
+ return delegate.process(value, context);
+ } catch (NoMatchingRuleException e) {
+ return alternative.process(value, context);
+ }
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java
new file mode 100644
index 00000000000..ca922f1720a
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java
@@ -0,0 +1,26 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import lombok.Value;
+
+import java.util.List;
+
+@Value(staticConstructor = "of")
+public class CompositeRule implements Rule {
+
+ List> subrules;
+
+ @Override
+ public RuleResult process(T value, C context) {
+ RuleResult result = RuleResult.noAction(value);
+
+ for (Rule subrule : subrules) {
+ result = result.mergeWith(subrule.process(value, context));
+
+ if (result.isReject()) {
+ return result;
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java
new file mode 100644
index 00000000000..c56c580d149
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java
@@ -0,0 +1,90 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.LookupResult;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class ConditionalRule implements Rule {
+
+ private final Schema schema;
+ private final RuleTree> ruleTree;
+
+ private final String modelVersion;
+ private final String analyticsKey;
+
+ public ConditionalRule(Schema schema,
+ RuleTree> ruleTree,
+ String analyticsKey,
+ String modelVersion) {
+
+ this.schema = Objects.requireNonNull(schema);
+
+ this.ruleTree = Objects.requireNonNull(ruleTree);
+ this.analyticsKey = StringUtils.defaultString(analyticsKey);
+ this.modelVersion = StringUtils.defaultString(modelVersion);
+ }
+
+ @Override
+ public RuleResult process(T value, C context) {
+ final List> schemaFunctions = schema.getFunctions();
+ final List matchers = schemaFunctions.stream()
+ .map(holder -> holder.getSchemaFunction().extract(
+ SchemaFunctionArguments.of(value, holder.getConfig(), context)))
+ .map(matcher -> StringUtils.defaultIfEmpty(matcher, SchemaFunction.UNDEFINED_RESULT))
+ .toList();
+
+ final LookupResult> lookupResult = ruleTree.lookup(matchers);
+ final RuleConfig ruleConfig = lookupResult.getValue();
+
+ final InfrastructureArguments infrastructureArguments =
+ InfrastructureArguments.builder()
+ .context(context)
+ .schemaFunctionResults(mergeWithSchema(schema, matchers))
+ .schemaFunctionMatches(mergeWithSchema(schema, lookupResult.getMatches()))
+ .ruleFired(ruleConfig.getCondition())
+ .analyticsKey(analyticsKey)
+ .modelVersion(modelVersion)
+ .build();
+
+ RuleResult result = RuleResult.noAction(value);
+ for (ResultFunctionHolder action : ruleConfig.getActions()) {
+ result = result.mergeWith(applyAction(action, result.getValue(), infrastructureArguments));
+
+ if (result.isReject()) {
+ return result;
+ }
+ }
+
+ return result;
+ }
+
+ private Map mergeWithSchema(Schema schema, List values) {
+ return IntStream.range(0, values.size())
+ .boxed()
+ .collect(Collectors.toMap(
+ idx -> schema.getFunctions().get(idx).getName(), values::get, (left, right) -> left));
+ }
+
+ private RuleResult applyAction(ResultFunctionHolder action,
+ T value,
+ InfrastructureArguments infrastructureArguments) {
+
+ final ResultFunctionArguments arguments = ResultFunctionArguments.of(
+ value, action.getConfig(), infrastructureArguments);
+
+ return action.getFunction().apply(arguments);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java
new file mode 100644
index 00000000000..918b544e086
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java
@@ -0,0 +1,12 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree;
+
+public interface ConditionalRuleFactory {
+
+ Rule create(Schema schema,
+ RuleTree> ruleTree,
+ String analyticsKey,
+ String modelVersion);
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java
new file mode 100644
index 00000000000..5e3198b8e24
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java
@@ -0,0 +1,62 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import lombok.EqualsAndHashCode;
+import org.apache.commons.collections4.ListUtils;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+@EqualsAndHashCode
+public class DefaultActionRule implements Rule {
+
+ private static final String RULE_NAME = "default";
+
+ private final List> actions;
+
+ private final String analyticsKey;
+ private final String modelVersion;
+
+ public DefaultActionRule(List> actions, String analyticsKey, String modelVersion) {
+ this.actions = ListUtils.emptyIfNull(actions);
+
+ this.analyticsKey = Objects.requireNonNull(analyticsKey);
+ this.modelVersion = Objects.requireNonNull(modelVersion);
+ }
+
+ @Override
+ public RuleResult process(T value, C context) {
+ RuleResult result = RuleResult.noAction(value);
+
+ for (ResultFunctionHolder action : actions) {
+ result = result.mergeWith(applyAction(action, result.getValue(), context));
+
+ if (result.isReject()) {
+ return result;
+ }
+ }
+
+ return result;
+ }
+
+ private RuleResult applyAction(ResultFunctionHolder action, T value, C context) {
+ final ResultFunctionArguments arguments = ResultFunctionArguments.of(
+ value, action.getConfig(), infrastructureArguments(context));
+
+ return action.getFunction().apply(arguments);
+ }
+
+ private InfrastructureArguments infrastructureArguments(C context) {
+ return InfrastructureArguments.builder()
+ .context(context)
+ .schemaFunctionResults(Collections.emptyMap())
+ .schemaFunctionMatches(Collections.emptyMap())
+ .ruleFired(RULE_NAME)
+ .analyticsKey(analyticsKey)
+ .modelVersion(modelVersion)
+ .build();
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java
new file mode 100644
index 00000000000..c9ea02dc9f4
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java
@@ -0,0 +1,12 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import lombok.Value;
+
+@Value(staticConstructor = "create")
+public class NoOpRule implements Rule {
+
+ @Override
+ public RuleResult process(T value, C context) {
+ return RuleResult.noAction(value);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java
new file mode 100644
index 00000000000..c9d3c3383be
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java
@@ -0,0 +1,28 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import com.iab.openrtb.request.BidRequest;
+import lombok.Builder;
+import lombok.Value;
+import lombok.experimental.Accessors;
+import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext;
+
+import java.time.Instant;
+
+@Builder
+@Accessors(fluent = true)
+@Value(staticConstructor = "of")
+public class PerStageRule {
+
+ private static final PerStageRule NO_OP = PerStageRule.builder()
+ .processedAuctionRequestRule(NoOpRule.create())
+ .build();
+
+ Instant timestamp;
+
+ Rule processedAuctionRequestRule;
+
+ public static PerStageRule noOp() {
+ return NO_OP;
+ }
+}
+
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java
new file mode 100644
index 00000000000..e3275ef0621
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java
@@ -0,0 +1,18 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import lombok.Value;
+import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList;
+
+import java.util.random.RandomGenerator;
+
+@Value(staticConstructor = "of")
+public class RandomWeightedRule implements Rule {
+
+ RandomGenerator random;
+ WeightedList> weightedList;
+
+ @Override
+ public RuleResult process(T value, C context) {
+ return weightedList.getForSeed(random.nextInt(weightedList.maxSeed())).process(value, context);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java
new file mode 100644
index 00000000000..1b369d52d9b
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java
@@ -0,0 +1,6 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+public interface Rule {
+
+ RuleResult process(T value, C context);
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java
new file mode 100644
index 00000000000..b123850bebb
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java
@@ -0,0 +1,6 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+public enum RuleAction {
+
+ NO_ACTION, UPDATE, REJECT
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java
new file mode 100644
index 00000000000..0c40c198e7c
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java
@@ -0,0 +1,14 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import lombok.Value;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder;
+
+import java.util.List;
+
+@Value(staticConstructor = "of")
+public class RuleConfig {
+
+ String condition;
+
+ List> actions;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java
new file mode 100644
index 00000000000..8967430982b
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java
@@ -0,0 +1,58 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import lombok.Value;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid;
+import org.prebid.server.util.ListUtil;
+
+import java.util.Collections;
+import java.util.List;
+
+@Value(staticConstructor = "of")
+public class RuleResult {
+
+ T value;
+
+ RuleAction action;
+
+ Tags analyticsTags;
+
+ List seatNonBid;
+
+ public RuleResult mergeWith(RuleResult other) {
+ final T value = other.getValue();
+ final RuleAction action = merge(this.action, other.getAction());
+ final Tags tags = TagsImpl.of(ListUtil.union(analyticsTags.activities(), other.analyticsTags.activities()));
+ final List seatNonBids = ListUtil.union(seatNonBid, other.seatNonBid);
+
+ return RuleResult.of(value, action, tags, seatNonBids);
+ }
+
+ private static RuleAction merge(RuleAction left, RuleAction right) {
+ if (left == RuleAction.REJECT || right == RuleAction.REJECT) {
+ return RuleAction.REJECT;
+ }
+ if (left == RuleAction.UPDATE || right == RuleAction.UPDATE) {
+ return RuleAction.UPDATE;
+ }
+ return RuleAction.NO_ACTION;
+ }
+
+ public boolean isReject() {
+ return action == RuleAction.REJECT;
+ }
+
+ public boolean isUpdate() {
+ return action == RuleAction.UPDATE;
+ }
+
+ public static RuleResult noAction(T value) {
+ return RuleResult.of(
+ value, RuleAction.NO_ACTION, TagsImpl.of(Collections.emptyList()), Collections.emptyList());
+ }
+
+ public static RuleResult rejected(Tags analyticsTags, List seatNonBids) {
+ return RuleResult.of(null, RuleAction.REJECT, analyticsTags, seatNonBids);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java
new file mode 100644
index 00000000000..2083983091f
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java
@@ -0,0 +1,11 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules;
+
+import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction;
+
+public interface StageSpecification {
+
+ SchemaFunction schemaFunctionByName(String name);
+
+ ResultFunction resultFunctionByName(String name);
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java
new file mode 100644
index 00000000000..e7fea8db54d
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java
@@ -0,0 +1,8 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.exception;
+
+public class InvalidMatcherConfiguration extends RuntimeException {
+
+ public InvalidMatcherConfiguration(String message) {
+ super(message);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java
new file mode 100644
index 00000000000..356f7ef53b4
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java
@@ -0,0 +1,8 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.exception;
+
+public class InvalidResultFunctionException extends RuntimeException {
+
+ public InvalidResultFunctionException(String function) {
+ super("Invalid result function: " + function);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java
new file mode 100644
index 00000000000..f1b0f05f7b6
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java
@@ -0,0 +1,8 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.exception;
+
+public class InvalidSchemaFunctionException extends RuntimeException {
+
+ public InvalidSchemaFunctionException(String function) {
+ super("Invalid schema function: " + function);
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java
new file mode 100644
index 00000000000..fc1869d3d14
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java
@@ -0,0 +1,10 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.exception;
+
+import org.prebid.server.hooks.modules.rule.engine.core.util.NoMatchingValueException;
+
+public class NoMatchingRuleException extends NoMatchingValueException {
+
+ public NoMatchingRuleException() {
+ super("No matching rule found");
+ }
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java
new file mode 100644
index 00000000000..f30eb769ddf
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java
@@ -0,0 +1,23 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.result;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.util.Map;
+
+@Value
+@Builder
+public class InfrastructureArguments {
+
+ C context;
+
+ Map schemaFunctionResults;
+
+ Map schemaFunctionMatches;
+
+ String ruleFired;
+
+ String analyticsKey;
+
+ String modelVersion;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java
new file mode 100644
index 00000000000..831a85c6af9
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java
@@ -0,0 +1,11 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.result;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult;
+
+public interface ResultFunction {
+
+ RuleResult apply(ResultFunctionArguments arguments);
+
+ void validateConfig(ObjectNode config);
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java
new file mode 100644
index 00000000000..abe28017df8
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java
@@ -0,0 +1,14 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.result;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class ResultFunctionArguments {
+
+ T operand;
+
+ ObjectNode config;
+
+ InfrastructureArguments infrastructureArguments;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java
new file mode 100644
index 00000000000..5b7476511d4
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java
@@ -0,0 +1,14 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.result;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class ResultFunctionHolder {
+
+ String name;
+
+ ResultFunction function;
+
+ ObjectNode config;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java
new file mode 100644
index 00000000000..64ec5aa6e23
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java
@@ -0,0 +1,11 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.schema;
+
+import lombok.Value;
+
+import java.util.List;
+
+@Value(staticConstructor = "of")
+public class Schema {
+
+ List> functions;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java
new file mode 100644
index 00000000000..887d76eb9ec
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java
@@ -0,0 +1,12 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.schema;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public interface SchemaFunction {
+
+ String UNDEFINED_RESULT = "undefined";
+
+ String extract(SchemaFunctionArguments arguments);
+
+ void validateConfig(ObjectNode config);
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java
new file mode 100644
index 00000000000..062d1776d0f
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java
@@ -0,0 +1,14 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.schema;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class SchemaFunctionArguments {
+
+ T operand;
+
+ ObjectNode config;
+
+ C context;
+}
diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java
new file mode 100644
index 00000000000..2264cfc2088
--- /dev/null
+++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java
@@ -0,0 +1,14 @@
+package org.prebid.server.hooks.modules.rule.engine.core.rules.schema;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class SchemaFunctionHolder