Quarkus Funqy on Localhost: CloudEvents Before Knative
Keep the Quarkus dev loop, prove HTTP and CloudEvent contracts locally, and save the Knative broker wiring for later.
Event-driven serverless tutorials often jump from one annotation straight into a cluster. I do not like that learning curve. Knative isn’t trivial and there is a lot to learn before we even jump into that:
Can I call the function locally with normal HTTP?
Can I prove the input and output shape without a platform in the middle?
Can I trigger a function by CloudEvent type instead of only by URL path?
Can I read CloudEvent metadata at the edge without turning the whole app into provider-specific glue?
That is the local loop we build here.
Funqy is intentionally small. A function gets one optional input parameter, may or may not return a response, and stays portable across multiple serverless targets. The Funqy HTTP binding lets us invoke those functions on localhost. The Funqy Knative Events binding adds CloudEvent routing on top of that local HTTP model, including binary and structured mode.
One slightly awkward detail first. As of June 8, 2026, the official Funqy, Funqy HTTP, and Funqy Knative Events guides still mark these integrations as preview. I am fine with that, but it does mean I want a tight local proof loop before I trust my memory or a YAML pile.
So we build a small alert-routing sample called funqy-alert-pipeline. It stays fully local. We do not run Knative itself here. The broker hop is a separate system concern. On localhost, we prove the function contract first.
What we build
We build a Quarkus app with four Funqy functions:
previewAlertgives us a full route preview through an HTTP GET with query parameters.ingestAlertaccepts a JSON alert, normalizes it, and classifies its severity.scoreAlertis triggered by a CloudEvent type that we map inapplication.properties.routeAlertis triggered by@CloudEventMappingand reads CloudEvent metadata through@Context.
By the end, we will do four concrete checks:
Call a Funqy function on
localhostwith query parameters.POST plain JSON to a function path.
Send a binary CloudEvent to
/and let the CloudEvent type choose the function.Send a structured CloudEvent and read event metadata inside the function.
What you need
JDK 21 or newer
About ☕️☕️
Comfort with basic Quarkus dev mode
Three quick terms help here:
Funqy function - a Java method annotated with
@FunqBinary CloudEvent - metadata in
Ce-*headers, JSON data in the bodyStructured CloudEvent - metadata and JSON data together in one
application/cloudevents+jsonpayload
One piece is missing on purpose: the Knative Broker. In a real Knative deployment, that component forwards one function’s response event to the next trigger. We are not trying to fake all of that on localhost. We are locking down the contract each hop would use.
Create the project
Generate a project with the Knative Events binding and follow along or grab the full source from my Github repository:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.mainthread \
-DprojectArtifactId=funqy-alert-pipeline \
-Dextensions='funqy-knative-events' \
-DnoCode
cd funqy-alert-pipelineThis sample uses Java 21 on purpose:
<properties>
<maven.compiler.release>21</maven.compiler.release>
<quarkus.platform.version>3.36.1</quarkus.platform.version>
</properties>The generated dependencies stay small. quarkus-funqy-knative-events gives us the Funqy runtime plus the Knative CloudEvent binding. quarkus-arc backs CDI injection for our service.
Add the alert model
Funqy HTTP query mapping expects bean-style inputs for GET requests. That is why I use ordinary Java classes here instead of records. This part is plain on purpose. It keeps the GET path and the JSON path aligned, which matters more than showing off records in a demo.
Create src/main/java/com/mainthread/funqyalert/AlertEnvelope.java:
package com.mainthread.funqyalert;
import java.util.ArrayList;
import java.util.List;
public class AlertEnvelope {
private String service;
private String environment;
private String region;
private String summary;
private double errorRatePercent;
private int impactedCustomers;
private boolean acknowledged;
private String severity;
private int riskScore;
private String dedupeKey;
private List<String> checkpoints = new ArrayList<>();
public AlertEnvelope() {
}
public AlertEnvelope(AlertEnvelope other) {
this.service = other.service;
this.environment = other.environment;
this.region = other.region;
this.summary = other.summary;
this.errorRatePercent = other.errorRatePercent;
this.impactedCustomers = other.impactedCustomers;
this.acknowledged = other.acknowledged;
this.severity = other.severity;
this.riskScore = other.riskScore;
this.dedupeKey = other.dedupeKey;
this.checkpoints = new ArrayList<>(other.checkpoints);
}
public void addCheckpoint(String checkpoint) {
this.checkpoints.add(checkpoint);
}
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public String getEnvironment() {
return environment;
}
public void setEnvironment(String environment) {
this.environment = environment;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public double getErrorRatePercent() {
return errorRatePercent;
}
public void setErrorRatePercent(double errorRatePercent) {
this.errorRatePercent = errorRatePercent;
}
public int getImpactedCustomers() {
return impactedCustomers;
}
public void setImpactedCustomers(int impactedCustomers) {
this.impactedCustomers = impactedCustomers;
}
public boolean isAcknowledged() {
return acknowledged;
}
public void setAcknowledged(boolean acknowledged) {
this.acknowledged = acknowledged;
}
public String getSeverity() {
return severity;
}
public void setSeverity(String severity) {
this.severity = severity;
}
public int getRiskScore() {
return riskScore;
}
public void setRiskScore(int riskScore) {
this.riskScore = riskScore;
}
public String getDedupeKey() {
return dedupeKey;
}
public void setDedupeKey(String dedupeKey) {
this.dedupeKey = dedupeKey;
}
public List<String> getCheckpoints() {
return checkpoints;
}
public void setCheckpoints(List<String> checkpoints) {
this.checkpoints = checkpoints == null ? new ArrayList<>() : new ArrayList<>(checkpoints);
}
}Then create src/main/java/com/mainthread/funqyalert/RoutingDecision.java:
package com.mainthread.funqyalert;
import java.util.ArrayList;
import java.util.List;
public class RoutingDecision {
private String service;
private String environment;
private String region;
private String summary;
private String severity;
private int riskScore;
private String destinationTeam;
private boolean pageImmediately;
private int acknowledgeWithinMinutes;
private String runbookUrl;
private String rationale;
private String triggeringEventId;
private String triggeringEventSource;
private List<String> checkpoints = new ArrayList<>();
public RoutingDecision() {
}
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public String getEnvironment() {
return environment;
}
public void setEnvironment(String environment) {
this.environment = environment;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getSeverity() {
return severity;
}
public void setSeverity(String severity) {
this.severity = severity;
}
public int getRiskScore() {
return riskScore;
}
public void setRiskScore(int riskScore) {
this.riskScore = riskScore;
}
public String getDestinationTeam() {
return destinationTeam;
}
public void setDestinationTeam(String destinationTeam) {
this.destinationTeam = destinationTeam;
}
public boolean isPageImmediately() {
return pageImmediately;
}
public void setPageImmediately(boolean pageImmediately) {
this.pageImmediately = pageImmediately;
}
public int getAcknowledgeWithinMinutes() {
return acknowledgeWithinMinutes;
}
public void setAcknowledgeWithinMinutes(int acknowledgeWithinMinutes) {
this.acknowledgeWithinMinutes = acknowledgeWithinMinutes;
}
public String getRunbookUrl() {
return runbookUrl;
}
public void setRunbookUrl(String runbookUrl) {
this.runbookUrl = runbookUrl;
}
public String getRationale() {
return rationale;
}
public void setRationale(String rationale) {
this.rationale = rationale;
}
public String getTriggeringEventId() {
return triggeringEventId;
}
public void setTriggeringEventId(String triggeringEventId) {
this.triggeringEventId = triggeringEventId;
}
public String getTriggeringEventSource() {
return triggeringEventSource;
}
public void setTriggeringEventSource(String triggeringEventSource) {
this.triggeringEventSource = triggeringEventSource;
}
public List<String> getCheckpoints() {
return checkpoints;
}
public void setCheckpoints(List<String> checkpoints) {
this.checkpoints = checkpoints == null ? new ArrayList<>() : new ArrayList<>(checkpoints);
}
}AlertEnvelope is the event shape that moves through the local pipeline. RoutingDecision is the final answer we care about.
Add the pipeline service
I keep the workflow logic in one CDI bean because the Funqy methods should stay thin. That gives us one place to normalize input, classify severity, calculate risk, and pick a team.
Create src/main/java/com/mainthread/funqyalert/AlertPipelineService.java:
package com.mainthread.funqyalert;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AlertPipelineService {
private static final Set<String> SUPPORTED_ENVIRONMENTS = Set.of("prod", "staging", "dev");
public RoutingDecision preview(AlertEnvelope alert) {
AlertEnvelope ingested = ingest(alert);
AlertEnvelope scored = score(ingested);
return route(scored, "preview-alert", "localhost");
}
public AlertEnvelope ingest(AlertEnvelope alert) {
AlertEnvelope normalized = new AlertEnvelope(alert);
normalized.setService(normalizeRequired("service", alert.getService()));
normalized.setEnvironment(normalizeEnvironment(alert.getEnvironment()));
normalized.setRegion(normalizeRequired("region", alert.getRegion()));
normalized.setSummary(normalizeRequired("summary", alert.getSummary()));
if (alert.getErrorRatePercent() < 0) {
throw new IllegalArgumentException("errorRatePercent must be zero or greater.");
}
if (alert.getImpactedCustomers() < 0) {
throw new IllegalArgumentException("impactedCustomers must be zero or greater.");
}
normalized.setSeverity(classifySeverity(normalized));
normalized.setDedupeKey(buildDedupeKey(normalized));
normalized.setCheckpoints(List.of("validated", "ingested"));
return normalized;
}
public AlertEnvelope score(AlertEnvelope alert) {
AlertEnvelope scored = alert.getSeverity() == null || alert.getDedupeKey() == null
? ingest(alert)
: new AlertEnvelope(alert);
int riskScore = switch (scored.getSeverity()) {
case "critical" -> 85;
case "high" -> 68;
case "medium" -> 42;
default -> 18;
};
if (!scored.isAcknowledged()) {
riskScore += 12;
}
riskScore += Math.min(15, scored.getImpactedCustomers() / 200);
riskScore += Math.min(8, (int) Math.floor(scored.getErrorRatePercent()));
scored.setRiskScore(Math.min(riskScore, 100));
List<String> checkpoints = scored.getCheckpoints();
if (!checkpoints.contains("scored")) {
checkpoints.add("scored");
}
return scored;
}
public RoutingDecision route(AlertEnvelope alert, String eventId, String eventSource) {
AlertEnvelope scored = alert.getRiskScore() == 0 ? score(alert) : new AlertEnvelope(alert);
RoutingDecision decision = new RoutingDecision();
decision.setService(scored.getService());
decision.setEnvironment(scored.getEnvironment());
decision.setRegion(scored.getRegion());
decision.setSummary(scored.getSummary());
decision.setSeverity(scored.getSeverity());
decision.setRiskScore(scored.getRiskScore());
decision.setDestinationTeam(selectTeam(scored));
decision.setPageImmediately(scored.getRiskScore() >= 70);
decision.setAcknowledgeWithinMinutes(ackDeadline(scored.getRiskScore()));
decision.setRunbookUrl("https://runbooks.example.com/" + scored.getService() + "/" + scored.getSeverity());
decision.setRationale(buildRationale(scored));
decision.setTriggeringEventId(eventId);
decision.setTriggeringEventSource(eventSource);
List<String> checkpoints = scored.getCheckpoints();
if (!checkpoints.contains("routed")) {
checkpoints.add("routed");
}
decision.setCheckpoints(checkpoints);
return decision;
}
private String normalizeRequired(String name, String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " is required.");
}
return value.trim().toLowerCase(Locale.ROOT);
}
private String normalizeEnvironment(String value) {
String environment = normalizeRequired("environment", value);
if (!SUPPORTED_ENVIRONMENTS.contains(environment)) {
throw new IllegalArgumentException("environment must be one of prod, staging, or dev.");
}
return environment;
}
private String classifySeverity(AlertEnvelope alert) {
if ("prod".equals(alert.getEnvironment())
&& (alert.getErrorRatePercent() >= 5.0 || alert.getImpactedCustomers() >= 1_000)) {
return "critical";
}
if (alert.getErrorRatePercent() >= 2.0 || alert.getImpactedCustomers() >= 250) {
return "high";
}
if (alert.getErrorRatePercent() >= 0.5 || !alert.isAcknowledged()) {
return "medium";
}
return "low";
}
private String buildDedupeKey(AlertEnvelope alert) {
String summarySlug = alert.getSummary().replaceAll("[^a-z0-9]+", "-").replaceAll("(^-|-$)", "");
return alert.getService() + ":" + alert.getRegion() + ":" + summarySlug;
}
private String selectTeam(AlertEnvelope alert) {
return switch (alert.getSeverity()) {
case "critical" -> alert.getService() + "-oncall";
case "high" -> alert.getService() + "-primary";
case "medium" -> alert.getService() + "-triage";
default -> alert.getService() + "-backlog";
};
}
private int ackDeadline(int riskScore) {
if (riskScore >= 85) {
return 5;
}
if (riskScore >= 70) {
return 10;
}
if (riskScore >= 45) {
return 30;
}
return 240;
}
private String buildRationale(AlertEnvelope alert) {
return "Route to " + selectTeam(alert)
+ " because " + alert.getEnvironment()
+ " is seeing "
+ alert.getErrorRatePercent()
+ "% errors with "
+ alert.getImpactedCustomers()
+ " impacted customers.";
}
}Let me explain a few of the choices here:
The service always normalizes the incoming alert before it trusts the fields.
Severity and routing are explicit and easy to read, which matters more than clever-looking code in a demo.
preview()runs the whole path at once so we have one fast worked slice before we widen the scope.
I like that last part because it gives us one rewarding call early. After that, we can zoom in on the separate Funqy pieces without losing the plot.
Add the Funqy functions
Next, wire the service into a Funqy component.
Create src/main/java/com/mainthread/funqyalert/AlertFunctions.java:
package com.mainthread.funqyalert;
import org.jboss.logging.Logger;
import io.quarkus.funqy.Context;
import io.quarkus.funqy.Funq;
import io.quarkus.funqy.knative.events.CloudEvent;
import io.quarkus.funqy.knative.events.CloudEventMapping;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AlertFunctions {
private static final Logger LOG = Logger.getLogger(AlertFunctions.class);
private final AlertPipelineService pipeline;
public AlertFunctions(AlertPipelineService pipeline) {
this.pipeline = pipeline;
}
@Funq
public RoutingDecision previewAlert(AlertEnvelope alert) {
LOG.infof("previewAlert for service=%s env=%s", alert.getService(), alert.getEnvironment());
return pipeline.preview(alert);
}
@Funq
public AlertEnvelope ingestAlert(AlertEnvelope alert) {
LOG.infof("ingestAlert for service=%s", alert.getService());
return pipeline.ingest(alert);
}
@Funq
public AlertEnvelope scoreAlert(AlertEnvelope alert) {
LOG.infof("scoreAlert for dedupeKey=%s", alert.getDedupeKey());
return pipeline.score(alert);
}
@Funq
@CloudEventMapping(trigger = "com.mainthread.alert.scored", responseSource = "routeAlert", responseType = "com.mainthread.alert.routed")
public RoutingDecision routeAlert(AlertEnvelope alert, @Context CloudEvent cloudEvent) {
LOG.infof("routeAlert from source=%s", cloudEvent.source());
return pipeline.route(alert, cloudEvent.id(), cloudEvent.source());
}
}These methods give us three useful invocation paths in one place:
previewAlertis the fast preview path.ingestAlertandscoreAlertlet us inspect intermediate event shapes directly.routeAlertshows the one place where runtime-specific context is useful: at the edge, when you actually care about event metadata.
The official Funqy guide makes the same point. You can inject contextual details, but you should keep that dependency narrow instead of smearing it across every function just because the annotation exists.
Configure one CloudEvent mapping in properties
I want this sample to show both mapping styles: configuration and annotation.
Add this to src/main/resources/application.properties:
quarkus.funqy.knative-events.mapping.scoreAlert.trigger=ingestAlert.output
quarkus.funqy.knative-events.mapping.scoreAlert.response-type=com.mainthread.alert.scored
quarkus.funqy.knative-events.mapping.scoreAlert.response-source=scoreAlert
quarkus.log.category."com.mainthread.funqyalert".level=INFOThe mapping chain is simple:
ingestAlertuses the default mapping. If it returns output, the response event type becomesingestAlert.output.scoreAlertis configured to trigger on that output event type and respond withcom.mainthread.alert.scored.routeAlertis mapped with@CloudEventMappingand responds withcom.mainthread.alert.routed.
In a real Knative deployment, a broker would hand those response events to the next trigger. Locally, we call each hop ourselves. That sounds less magical because it is less magical.
Run the app
Start Quarkus dev mode:
./mvnw quarkus:devProve the fast path with GET query mapping
I like to start here because the model clicks fast. Funqy binds query parameters into a bean-style input object, and the function name becomes the URL path.
Run this:
curl -s 'http://127.0.0.1:8080/previewAlert?service=payments&environment=prod®ion=us-east-1&summary=Checkout%20timeouts%20spreading&errorRatePercent=7.2&impactedCustomers=1800&acknowledged=false'You should get:
{
"service": "payments",
"environment": "prod",
"region": "us-east-1",
"summary": "checkout timeouts spreading",
"severity": "critical",
"riskScore": 100,
"destinationTeam": "payments-oncall",
"pageImmediately": true,
"acknowledgeWithinMinutes": 5,
"runbookUrl": "https://runbooks.example.com/payments/critical",
"rationale": "Route to payments-oncall because prod is seeing 7.2% errors with 1800 impacted customers.",
"triggeringEventId": "preview-alert",
"triggeringEventSource": "localhost",
"checkpoints": [
"validated",
"ingested",
"scored",
"routed"
]
}
That response gives us the core split right away:
Funqy HTTP is enough for a fast local loop.
The business logic can stay plain CDI code.
We do not need CloudEvent headers yet to prove the routing decision.
POST plain JSON to a function path
Next, narrow the scope. Instead of the full preview, hit only the first processing step:
curl -s -X POST http://127.0.0.1:8080/ingestAlert \
-H 'Content-Type: application/json' \
-d '{
"service":"Search",
"environment":"staging",
"region":"eu-west-1",
"summary":"Search latency climbing",
"errorRatePercent":2.4,
"impactedCustomers":420,
"acknowledged":false
}'The response should look like this:
{
"service": "search",
"environment": "staging",
"region": "eu-west-1",
"summary": "search latency climbing",
"errorRatePercent": 2.4,
"impactedCustomers": 420,
"acknowledged": false,
"severity": "high",
"riskScore": 0,
"dedupeKey": "search:eu-west-1:search-latency-climbing",
"checkpoints": [
"validated",
"ingested"
]
}This is where Funqy starts to feel different from Quarkus REST. A function path is a small function endpoint. It exists to expose an event contract. If you want resource modeling, that is a different job.
Trigger scoreAlert with a binary CloudEvent
Before you run this next call, make one prediction.
If the request goes to /, how does Quarkus know which function to invoke?
The answer is the CloudEvent type. We mapped scoreAlert to trigger on ingestAlert.output, so that type is the selector now.
Run this binary CloudEvent request:
curl -si -X POST http://127.0.0.1:8080/ \
-H 'Content-Type: application/json' \
-H 'Ce-Id: binary-score-1' \
-H 'Ce-Specversion: 1.0' \
-H 'Ce-Type: ingestAlert.output' \
-H 'Ce-Source: urn:test:binary' \
-d '{
"service":"catalog",
"environment":"prod",
"region":"us-west-2",
"summary":"catalog misses rising",
"errorRatePercent":3.2,
"impactedCustomers":640,
"acknowledged":false,
"severity":"high",
"dedupeKey":"catalog:us-west-2:catalog-misses-rising",
"checkpoints":["validated","ingested"]
}'Look at the headers first:
HTTP/1.1 200 OK
ce-id: 6e8e6603-3993-4347-86fc-bd21830bdf58
ce-specversion: 1.0
ce-source: scoreAlert
ce-type: com.mainthread.alert.scored
Content-Type: application/jsonAnd the body shows the enriched event:
{
"service": "catalog",
"environment": "prod",
"region": "us-west-2",
"summary": "catalog misses rising",
"errorRatePercent": 3.2,
"impactedCustomers": 640,
"acknowledged": false,
"severity": "high",
"riskScore": 86,
"dedupeKey": "catalog:us-west-2:catalog-misses-rising",
"checkpoints": [
"validated",
"ingested",
"scored"
]
}This is why the Knative Events binding is useful even on localhost. We can prove the trigger type and the returned event type without starting a broker.
Trigger routeAlert with a structured CloudEvent
For the last step, we move the metadata into the request body and let routeAlert read it through @Context CloudEvent.
Run this:
curl -si -X POST http://127.0.0.1:8080/ \
-H 'Content-Type: application/cloudevents+json' \
-d '{
"specversion":"1.0",
"id":"structured-route-1",
"source":"urn:test:structured",
"type":"com.mainthread.alert.scored",
"datacontenttype":"application/json",
"data":{
"service":"checkout",
"environment":"prod",
"region":"us-east-1",
"summary":"checkout retries spiraling",
"errorRatePercent":6.8,
"impactedCustomers":900,
"acknowledged":false,
"severity":"critical",
"riskScore":100,
"dedupeKey":"checkout:us-east-1:checkout-retries-spiraling",
"checkpoints":["validated","ingested","scored"]
}
}'The response is another structured CloudEvent:
HTTP/1.1 200 OK
Content-Type: application/cloudevents+json{
"datacontenttype": "application/json",
"data": {
"service": "checkout",
"environment": "prod",
"region": "us-east-1",
"summary": "checkout retries spiraling",
"severity": "critical",
"riskScore": 100,
"destinationTeam": "checkout-oncall",
"pageImmediately": true,
"acknowledgeWithinMinutes": 5,
"runbookUrl": "https://runbooks.example.com/checkout/critical",
"rationale": "Route to checkout-oncall because prod is seeing 6.8% errors with 900 impacted customers.",
"triggeringEventId": "structured-route-1",
"triggeringEventSource": "urn:test:structured",
"checkpoints": [
"validated",
"ingested",
"scored",
"routed"
]
},
"specversion": "1.0",
"id": "690c62ee-e636-4e56-a7fd-445d6b54196e",
"source": "routeAlert",
"type": "com.mainthread.alert.routed"
}This is where the boundary gets clearer:
The function code understands the event contract.
The runtime can route by CloudEvent type.
The next cross-function hop still belongs to the broker, not to localhost theater.
That is the split I want. We keep the local loop fast, and we still design the event envelope the real deployment will use.
Run the tests
The sample includes four @QuarkusTest checks that cover the exact paths we walked through:
./mvnw testThose tests cover:
GET query parameter mapping through
previewAlertplain JSON POST to
ingestAlertbinary CloudEvent routing to
scoreAlertstructured CloudEvent routing to
routeAlert
Where this breaks, and where it helps
Funqy stays portable by staying small.
If you need a rich REST surface with resource linking, cache headers, conditional requests, or broader HTTP semantics, use Quarkus REST instead.
If you need a real multi-hop event chain, you still need the platform piece that forwards returned events to the next trigger. Locally, this sample proves the contracts for those hops.
If your CloudEvent payload is not JSON, this binding is the wrong fit. The Knative Events guide is explicit that the data part must be JSON and must map to your Java types.
If you start injecting runtime-specific context everywhere, your portable function story gets worse quickly. Keep that dependency narrow.
That is why I like this as a local-first tutorial. You learn the useful model, and nobody has to pretend the platform part comes for free.
Close the loop
We started with a small goal: prove the event contract before touching Knative. And that is what we did:
We built a Quarkus Funqy app that:
answers a fast preview request on
localhostaccepts plain JSON through a function path
routes a binary CloudEvent by type
routes a structured CloudEvent and reads event metadata inside the function
The model I keep in my head is simple. Funqy gives you tiny, portable function entry points. The HTTP binding makes them easy to test locally. The Knative Events binding makes CloudEvent type the function selector. The broker is still a separate system, which is good because it keeps the local loop honest.


