5G Destekli En Uygun Fiyatlı 3 Telefon!

1 Nisan 2026 itibarıyla 5G teknolojisi resmen kullanıma açılıyor. Yarın mobil internet deneyimi tamamen değişiyor. Bu deneyimi uygun fiyata elde etmek istiyorsan videodaki telefonlar tam sana göre.

Something Felt Wrong With My OpenClaw Setup. I Was Right

The fan wasn’t loud. That was the first thing.

It should have been. The board was under load, the enclosure had poor airflow, and I had a stack of processes I barely remembered configuring. But it sat there, almost polite. A faint hum, like it was pretending to work.

That’s when the feeling started.

Not panic. Not even suspicion. Just a small fracture in trust.

Most people ignore that moment. They move on, open another tab, check a dashboard, convince themselves everything is fine because nothing is obviously on fire.

I didn’t.

And it turns out that hesitation saved me from running a system I didn’t actually understand.

The Setup That Looked Fine

On paper, my OpenClaw setup was clean.

Fresh install. Dependencies resolved. Services running. No visible errors. The UI responded. Logs existed. Data moved where it was supposed to go.

It even survived a few reboots without collapsing into itself, which is more than you can say for half the things people throw together in a rush.

But something about it felt… staged.

You know that feeling when a room looks tidy but nothing is where it should be? Like someone cleaned it for you but didn’t know how you actually live there.

That’s what this was.

Processes were running, but I couldn’t explain why they were structured that way. Config files existed, but I didn’t remember writing half of them. Some defaults felt too convenient, like they were designed to keep things quiet rather than correct.

It worked. But it didn’t feel like mine.
That’s a problem.

The First Crack

The first real sign wasn’t dramatic.

A log entry repeated at odd intervals. Not enough to trigger alerts. Not frequent enough to look like a loop. Just… there. Drifting in and out like a signal trying not to be noticed.
At first I ignored it. Everyone ignores logs until they can’t.

Then I started tracing it.

It led me into a chain of dependencies I hadn’t fully mapped. One service feeding another. A script triggering a process that triggered something else. Layers stacked on top of assumptions.

And buried in there was a configuration mismatch.

Small. Easy to miss.

But it meant one part of the system believed it was operating in a different state than the rest. So it compensated. Quietly. Repeatedly. Forever.

That’s the kind of bug that doesn’t crash your setup.

It just slowly corrupts your understanding of it.

When “Working” Isn’t Safe

This is where most setups die, even if they keep running.

There’s a difference between a system that functions and a system that is correct.

OpenClaw sits in that dangerous middle ground.

It’s flexible enough to keep going when something is off. It doesn’t immediately punish misconfiguration. It adapts. It bends.

Which sounds good until you realize it’s hiding your mistakes.

My setup wasn’t broken in a visible way. It was drifting.

Data paths weren’t failing, they were rerouting. Services weren’t crashing, they were degrading. Outputs weren’t wrong enough to notice, just wrong enough to matter later.

That’s worse than failure.

Failure forces action. Drift lets you keep going.

The Moment It Clicked

I stopped looking at what the system was doing and started asking why it was doing it.

That shift matters more than any tool.

Instead of checking if services were active, I traced their origin. Instead of trusting defaults, I questioned them. Instead of assuming logs were noise, I treated them like evidence.

And the picture started to change.

What I thought was a clean setup was actually a layered patchwork of partial fixes, copied snippets, and assumptions carried over from earlier attempts.

Some of it was mine. Some of it wasn’t. Some of it I didn’t even remember applying.

It wasn’t malicious. It was just… accumulated.
That’s how most broken systems look. Not like disasters. Like sediment.

Where It Actually Broke

The core issue came down to synchronization.
Two parts of the system were supposed to agree on state. They didn’t.

One believed it had completed a process. The other never received confirmation. So it retried. Quietly. Indefinitely.

That created a ghost workload.

Resources were being consumed for tasks that had already finished. Logs filled with redundant entries. Timing drifted. Performance degraded just enough to be annoying but not enough to trigger alarms.

And because nothing outright failed, I kept trusting it.

That’s the trap.

The Slow Realization

Fixing it wasn’t a single moment. It was a series of small corrections.

Remove a redundant script. Restart a service. Watch behavior change. Undo something. Watch it stabilize. Then break again somewhere else.

Each adjustment revealed another layer.

It felt less like debugging and more like archaeology. Digging through decisions I had made without fully understanding their impact.
There’s a specific kind of frustration that comes from realizing you built the problem yourself.

Not because you’re careless. Because you moved too fast.

What Most People Do Instead

They patch.

They see something off and apply a fix on top of it. Maybe it works. Maybe it hides the issue. Either way, they move on.

That’s how systems become unfixable.

If you never stop to understand the root, you end up with a structure that only functions under very specific conditions. Change anything and it collapses.

OpenClaw doesn’t protect you from that. It enables it.

Which is why so many setups feel fragile even when they technically work.

The Clean Break

At some point, I stopped trying to repair it incrementally.

I stepped back and rebuilt parts of it from scratch. Not everything. Just the sections I no longer trusted.

This time, I documented every step. Not in a neat, polished way. Just enough to know why something existed.

That changed everything.

Because now when something felt off, I had context. I could trace decisions instead of guessing them.

The system became slower to build but faster to trust.

The Guide I Wish I Had

I ended up writing down the full recovery process. Not as a polished tutorial, but as a map of where things actually go wrong and how to pull them back into alignment.

If you’re in that same place where something feels off but you can’t prove it yet, this is where I’d point you.
It’s not about quick fixes. It’s about understanding why your setup drifted in the first place and how to bring it back without stacking more problems on top.

The Pattern Behind the Problem

This wasn’t just about OpenClaw.

It exposed a pattern I’ve seen across almost every system people build under pressure.

You start with clarity. Then you iterate. Then you patch. Then you forget why something was added. Then you trust it anyway.

Eventually, the system becomes something you operate but don’t understand.

That’s when it becomes dangerous.

Not because it will fail immediately. Because it will fail quietly.

What I Do Differently Now

I don’t trust “working” anymore.

I trust traceability. I trust simplicity. I trust systems that I can explain without opening five tabs.

If something feels off, I stop. Not later. Not after one more tweak. Right there.

That instinct is more valuable than any tool.

And it’s easy to ignore because it doesn’t look urgent.

The Part That Stays With You

The setup is stable now. Cleaner. Predictable.

But that initial feeling hasn’t gone away.

It shows up in other places now. Different projects. Different stacks.

A slight hesitation. A moment where something doesn’t sit right.

I’ve learned not to override it.

Because most of the time, it’s pointing at something real.

Not obvious. Not loud. But real enough to matter later.

And once you start listening to it, you realize how many systems are quietly wrong.

I built a free customs calculation platform with 140 features (240 countries)

Why I built POTAL

Customs calculation infrastructure shouldn’t cost $4,000/mo. I compiled every feature from 10 competitors — about 140 in total. The most feature-rich platform has ~30 features.

POTAL offers all 140 features for free. Forever.

For developers

  • REST API + SDKs (Node.js, Python)
  • AI Agent Integration (ChatGPT, Claude, Gemini via MCP)
  • Batch classification (bulk processing)
  • 240 countries, 51 languages
  • WCO GRI classification rules in code

Key features

HS Code classification (AI + image), Total Landed Cost, FTA origin determination, sanctions screening, denied party screening, export controls, de minimis thresholds, customs document generation, real-time currency conversion, dashboard + analytics.

Check it out: potal.app
Feedback: potal.app/community

Do not train the IA, let the AI train you

I recently found myself in a situation I hadn’t experienced in about three years: having the opportunity and the time to learn a new language from scratch.

The company I work for has several developers with extensive experience in Rust, and the internal community is growing in a very positive way, creating an ideal environment for learning and collaboration.

However, on a personal level, I felt a bit “rusty” when it came to sitting down to study. I had been working for the same client for over three years using technologies like React and Ruby on Rails within a massive codebase. When you work on a project of that magnitude, the primary focus is gaining ownership as quickly as possible; only then do you get the chance to innovate with new technologies, if the architecture allows.

As I was saying, this led me to a crossroads: I didn’t know where to begin. Everything felt like an uphill battle.

I started by scouring all the documentation I could find. I reached the official Rust Book, but I still felt a sense of cognitive overload. I’ve always been a “learning by doing” type of person, so a lightbulb went off: “Why don’t I ask the AI to bootstrap a project for me and learn as it generates the code?” WRONG.

That was not only a mistake but also the most passive approach I could have taken. Having an AI generate code for you while learning might help you understand project scaffolding or basic syntax, but it stops there. It lacks the “muscle memory” required for deep learning.

Luckily, I realized this quickly. My “Plan B” was to return to what worked when I started in web development: video tutorials. However, I discarded that idea because high-quality Udemy courses were around $60, and most YouTube content felt deprecated or outdated.

Then I thought of a different approach… What if, instead of watching the AI generate a project for me, I built the project increment by increment, using the AI as a real-time mentor?

The main challenge was avoiding the “autopilot” trap. To truly engage, I had to disable GitHub Copilot’s inline suggestions. I needed to write every line myself.

Since the AI already had the context of the initial project it had bootstrapped, I asked it to guide me step-by-step through a from-scratch implementation. Immediately, I noticed a shift: I was coding more intentionally. Most importantly, I had a “tutor” available 24/7 to answer foundational queries with full context of my specific implementation.

This shift in perspective transformed everything. By moving AI from the role of “creator” to that of “mentor,” I regained control of my learning curve. We often focus on how to train models or how to get them to give us the “right” answer, but we forget that their greatest value lies in their ability to explain the reasons behind things.

If you’re starting a new technical career, don’t let AI do all the heavy lifting. Don’t train AI to do your job; let AI train you to be a better developer.

Serverless applications on AWS with Lambda using Java 25, API Gateway and Aurora DSQL – Part 3 Introducing Lambda SnapStart

Introduction

In part 1, we introduced our sample application. In part 2, we measured the performance (cold and warm start times) of the Lambda function without any optimizations. We observed quite a large cold start time, especially if we use the Hibernate ORM framework. Using this framework also significantly increases the artifact size. In this article, we’ll introduce AWS Lambda SnapStart as one of the approaches to reducing the cold start times of the Lambda function. We’ll also provide the cold and warm start measurements of the sample application when the SnapStart is enabled for the Lambda function.

AWS Lambda SnapStart

As we saw in part 2, without any optimizations, Lambda performance measurements showed quite high values, especially for the cold start times. The article Understanding the Lambda execution environment lifecycle provides a good overview of this topic. Lambda SnapStart is one of the optimization approaches to reduce the cold start times.

Lambda SnapStart can provide a start time of a Lambda function of less than one second. SnapStart simplifies the development of responsive and scalable applications without provisioning resources or implementing complex performance optimizations.

The largest portion of startup latency (often referred to as cold start time) is the time Lambda spends initializing the function. This includes loading the function code, starting the runtime, and initializing the function code. With SnapStart, Lambda initializes our function when we publish a function version. Lambda takes a Firecracker microVM snapshot of the memory and disk state of the initialized execution environment. Then it encrypts the snapshot and intelligently caches it to optimize retrieval latency.

To ensure reliability, Lambda manages multiple copies of each snapshot. Lambda automatically patches snapshots and their copies with the latest runtime and security updates. When we invoke the function version, Lambda restores a new execution environment from the cached snapshot. This happens instead of initializing it from scratch, which improves startup latency. More information can be found in the article Reducing Java cold starts on AWS Lambda functions with SnapStart. You can find more information about the Lambda SnapStart in the article Under the hood: how AWS Lambda SnapStart optimizes function startup latency. I have published the whole series about Lambda SnapStart for Java applications.

Measurements of cold and warm start times of the Lambda function of the sample application with JDBC and Hikari connection pool

We’ll reuse the sample application from part 1 and do exactly the same performance measurement as we described in part 2. We’ll measure the performance of the GetProductByIdJava25WithDSQL Lambda function mapped to the GetProductByIdHandler. We will trigger it by invoking curl -H “X-API-Key: a6ZbcDefQW12BN56WEDQ25” https://{$API_GATEWAY_URL}/prod/products/1 .

One important aspect is that we instantiate Jackson ObjectMapper and ProductDao directly in the static initializer block of the GetProductByIdHandler Lambda function:

public class GetProductByIdHandler
        implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ProductDao productDao= new ProductDao();
...

When you create an ObjectMapper for the first time, it initializes a lot of other classes. As a part of this process, it instantiates a lot of singletons. It takes, depending on the hardware, more than a hundred milliseconds. If you create the second ObjectMapper in the same Java process, it takes only 1 millisecond because all the singletons are already there.  By moving the ObjectMapper instantiation to the static initializer block of the Lambda function, we decrease the cold start time. The reason for that is that this initialized object becomes a part of the SnapStart snapshot.

The same is true for ProductDao, especially taking into account that we directly preinitialize DsqlDataSourceConfig there:

public class ProductDao {

private static final DsqlDataSourceConfig dsqlDataSourceConfig=new DsqlDataSourceConfig();

....

This, in turn, loads a lot of classes and creates the Hikari Data Source. Moreover, it creates the Hikari connection pool as well. The part of the process is to search for the available JDBC driver. In our case, the PostgreSQL database driver will be found and loaded. Then the initialization of the database connection to Aurora DSQL happens, and this connection is added to the connection pool. We configured the pool size to be 1, because this is enough for the single-thread Lambda function. That’s why exactly one database connection will be created. With that, the database connection is ready to be reused. All of these become a part of the SnapStart snapshot:

public class DsqlDataSourceConfig {

private static final String AURORA_DSQL_CLUSTER_ENDPOINT = System.getenv("AURORA_DSQL_CLUSTER_ENDPOINT");

private static final String JDBC_URL = "jdbc:aws-dsql:postgresql://"
    + AURORA_DSQL_CLUSTER_ENDPOINT
     + ":5432/postgres?sslmode=verify-full&sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory"
 + "&token-duration-secs=900";


private static HikariDataSource hds;
static {    
   var config = new HikariConfig();
   config.setUsername("admin");
   config.setJdbcUrl(JDBC_URL);
   config.setMaxLifetime(1500 * 1000); // pool connection expiration time in milli seconds, default 30
    config.setMaximumPoolSize(1); // default is 10
    hds = new HikariDataSource(config);
}

Also, please make sure that we have enabled Lambda SnapStart in template.yaml as shown below:

Globals:
  Function:
    Handler: 
    SnapStart:
      ApplyOn: PublishedVersions 
    ....
    Environment:
      Variables:
        JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"

Please note that I measured only the performance of the Lambda function. On top of that comes also the latency of the trigger – in our case, the API Gateway REST API.

Please also note the effect of the Lambda SnapStart snapshot tiered cache. This means that in the case of SnapStart activation, we get the largest cold starts during the first measurements. Due to the tiered cache, the subsequent cold starts will have lower values. For more details about the technical implementation of AWS SnapStart and its tiered cache, I refer you to the presentation by Mike Danilov: “AWS Lambda Under the Hood”. Please also read the already mentioned article Under the hood: how AWS Lambda SnapStart optimizes function startup latency. Therefore, I will present the Lambda performance measurements with SnapStart being activated for 2 cases:

  • For all approximately 100 cold start times (labelled as all in the table)
  • For the last approximately 70 (labelled as last 70 in the table). With that, the effect of the snapshot tiered cache becomes visible to you. Depending on how often the respective Lambda function is updated and thus some layers of the cache are invalidated, a Lambda function can experience thousands or tens of thousands of cold starts during its life cycle, so that the first longer-lasting cold starts no longer carry much weight.

To show the impact of the SnapStart, we’ll also present the Lambda performance measurements without SnapStart being activated from part 2.

I did the measurements with java:25.v19 Amazon Corretto version, and the deployed artifact size of this application was 42.333 KB.

Cold (c) and warm (w) start time with -XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation in ms:

Approach c p50 c p75 c p90 c p99 c p99.9 c max w p50 w p75 w p90 w p99 w p99.9 w max
No SnapStart enabled 2336 2453 2827 3026 3131 3132 4.84 5.29 5.73 8.88 195.38 531
SnapStart enabled but no priming applied, all 970 1058 1705 1726 1734 1735 4.92 5.33 5.86 9.84 198.52 1134
SnapStart enabled but no priming applied, last 70 901 960 1061 1212 1212 1212 4.84 5.29 5.77 9.54 196.94 719

Measurements of cold and warm start times of the Lambda function of the sample application with Hibernate and Hikari connection pool

The same as mentioned above holds true for the sample application from part 1, using Hibernate instead of JDBC. We will measure the performance of our GetProductByIdJava25WithHibernateAndDSQL Lambda function mapped to the GetProductByIdHandler. We will trigger it by invoking curl -H “X-API-Key: a6ZbcDefQW12BN56WEHADQ25” https://{$API_GATEWAY_URL}/prod/products/1.

The most important difference is that in the ProductDao, we create the Hibernate Session Factory

public class ProductDao {
  private static final SessionFactory sessionFactory=   HibernateUtils.getSessionFactory();
....

In HibernateUtils, we set the same Hikari connection pool properties as in the example above. We then pass those properties to the Hibernate configuration along with the classes annotated as entities. The final part is to build a Hibernate session factory.

public final class HibernateUtils {

private static final String AURORA_DSQL_CLUSTER_ENDPOINT = System.getenv("AURORA_DSQL_CLUSTER_ENDPOINT");

private static final String JDBC_URL = "jdbc:aws-dsql:postgresql://"
   + AURORA_DSQL_CLUSTER_ENDPOINT
   + ":5432/postgres?sslmode=verify-full&sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory"
   + "&token-duration-secs=900";

private static SessionFactory sessionFactory= getHibernateSessionFactory();

private HibernateUtils () {}

private static SessionFactory getHibernateSessionFactory () {           
   var settings = new Properties();
   settings.put("jakarta.persistence.jdbc.user", "admin");
   settings.put("jakarta.persistence.jdbc.url", JDBC_URL);
   settings.put("hibernate.connection.pool_size", 1);
   settings.put("hibernate.hikari.maxLifetime", 1500 * 1000);
   return new Configuration()
     .setProperties(settings)
     .addAnnotatedClass(Product.class)
     .buildSessionFactory();    
}

public static SessionFactory getSessionFactory() {
     return sessionFactory;
}
...

All these steps involve a lot of class loading and preinitialization: Hikari Data Source, Hibernate Configuration, and Session Factory. Moreover, it creates the Hikari connection pool as well. The part of the process is to search for the available JDBC driver. In our case, the PostgreSQL database driver will be found and loaded. Then the initialization of the database connection to Aurora DSQL happens, and this connection is added to the connection pool. We configured the pool size to be 1, because this is enough for the single-thread Lambda function. That’s why exactly one database connection will be created. With that, the database connection is ready to be reused. All of these become a part of the SnapStart snapshot.

I did the measurements with java:25.v19 Amazon Corretto version, and the deployed artifact size of this application was 42.333 KB.

Cold (c) and warm (w) start time with -XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation in ms:

Approach c p50 c p75 c p90 c p99 c p99.9 c max w p50 w p75 w p90 w p99 w p99.9 w max
No SnapStart enabled 6243 6625 7056 8480 8651 8658 5.46 5.96 6.50 9.77 200.10 707
SnapStart enabled but no priming applied, all 1277 1360 3050 3103 3200 3201 5.50 6.01 6.45 10.16 196.94 2349
SnapStart enabled but no priming applied, last 70 1258 1320 1437 1634 1634 1634 5.42 5.91 6.40 10.08 195.94 1093

Conclusion

In this article of the series, we introduced AWS Lambda SnapStart as one of the approaches to reduce the cold start times of the Lambda function. We observed that by enabling the SnapStart on the Lambda function, the cold start time goes down significantly for both sample applications. It’s especially noticeable when looking at the “last 70” measurements with the snapshot tiered cache effect. The biggest impact of just enabling the SnapStart is on the application using the Hibernate. But still, the cold start remains quite high. In the next article, we’ll explore the first Lambda SnapStart priming technique. I call it the database (in our case, Aurora DSQL) request priming. The goal of applying priming is to preload and preinitialize as much as possible in the SnapStart snapshot during the deployment phase. With that, all those things will already be available directly after the SnapStart snapshot restore.

Please also watch out for another series where I use a NoSQL serverless Amazon DynamoDB database instead of Aurora DSQL to do the same Lambda performance measurements.

If you like my content, please follow me on GitHub and give my repositories a star!

Please also check out my website for more technical content and upcoming public speaking activities.

Built a tool to auto-reply to emails. Now I reply to spam faster than real people.

Built a tool to auto reply to emails. Now I reply to spam faster than real people.

Was getting 30 cold emails a day minimum. Recruiters, sales pitches, people asking if I want to buy their course. Started replying manually with “not interested” but that ate 20 minutes every morning.

Thought hey I can automate this.

Version 1 was embarrassingly simple

Wrote a Python script that checked my Gmail every 5 minutes and sent “Thanks but not interested” to anything with keywords like “opportunity” or “partnership” in the subject.

import imaplib
import smtplib
from email.mime.text import MIMEText

imap = imaplib.IMAP4_SSL('imap.gmail.com')
imap.login('[email protected]', 'password')
imap.select('INBOX')

status, messages = imap.search(None, 'UNSEEN SUBJECT "opportunity"')
for num in messages[0].split():
    msg = MIMEText('Thanks but not interested')
    msg['Subject'] = 'Re: ...'
    smtp = smtplib.SMTP('smtp.gmail.com', 587)
    smtp.sendmail('[email protected]', sender, msg.as_string())

Ran for 2 days.

Then I auto declined a job I actually wanted. Oops.

Tried adding a whitelist

Figured I needed to NOT reply to:

  • Domains I’ve emailed before
  • Anyone in my contacts
  • Emails with “interview” or “position” in subject

That helped until I replied to a vendor my boss was negotiating with. That conversation was awkward.

Current version pattern matches the body

Now it parses email content looking for:

  • “I came across your profile”
  • “quick call to discuss”
  • Links to calendly or meeting schedulers
  • Generic “reaching out” phrases

If 3+ patterns match and sender not in whitelist, auto sends “not currently looking for new opportunities”.

spam_patterns = [
    "came across your profile",
    "quick call",
    "calendly.com",
    "reaching out",
    "opportunity that might interest"
]

matches = sum(1 for pattern in spam_patterns if pattern in body.lower())
if matches >= 3 and sender not in whitelist:
    send_auto_reply(sender)

Running for 3 weeks now. Handled 180 cold emails. Only 1 false positive where I replied to a conference organizer I should have responded to manually.

Still kinda weird that my script is more polite to strangers than I am.

Adding Cloudflare Turnstile to Zoho Form HTML Exports

Zoho Forms lets you export your form as a standalone HTML package — but that exported form posts directly to Zoho’s servers with no spam protection. This guide walks you through adding Cloudflare Turnstile using a single drop-in script, with optional server-side verification via a Cloudflare Worker.

What we’re building

Browser → Turnstile challenge → token issued
       → form submitted → Cloudflare Worker
       → token verified server-side
       → payload forwarded to Zoho

The library (zoho-turnstile.js) works in two modes:

  • Client-only — blocks submissions with no token. Stops browser-based bots. No backend needed.
  • Full verification — routes submissions through a Cloudflare Worker that verifies the token before anything reaches Zoho. Stops everything.

⚠️ Client-only mode cannot stop direct POST attacks. For production forms, full verification is strongly recommended.

Prerequisites

  • A Cloudflare account (free tier is fine)
  • A Zoho Form exported as HTML (index.html, css/form.css, js/validation.js)
  • A domain registered under Cloudflare (for full verification)

Step 1 — Get a Cloudflare Turnstile Site Key

  1. Go to dash.cloudflare.com → Turnstile → Add Site
  2. Enter your domain
  3. Choose widget type Managed
  4. Copy the Site Key and Secret Key — you’ll need both

Step 2 — Add zoho-turnstile.js to your form export

Drop zoho-turnstile.js into your form folder alongside validation.js:

contact_form/
├── index.html
├── zoho-turnstile.js   ← new
├── css/
│   └── form.css
└── js/
    └── validation.js

Step 3 — Initialise the library

Add the script to index.html inside <head>:

<script src="js/validation.js"></script>
<script src="zoho-turnstile.js"></script>   <!-- add this -->

Then initialise it before </body>, after your existing Zoho init vars:

<script>
  // Your existing Zoho vars — already in your export, don't duplicate
  var zf_MandArray = ["SingleLine", "Email", "MultiLine"];
  var zf_FieldArray = ["SingleLine", "Email", "MultiLine"];
  var isSalesIQIntegrationEnabled = false;
  var salesIQFieldsArray = [];

  // Turnstile init
  ZohoTurnstile.init({
    siteKey: 'YOUR_SITE_KEY'
  });
</script>

That’s it for client-only mode. The widget will appear above your submit button automatically.

Step 4 (optional) — Add server-side verification via Cloudflare Worker

Skip this if you’re using client-only mode. If you’re going to production, don’t skip this.

4a — Create the Worker

  1. In the Cloudflare dashboard → Workers & Pages → Create → Start with Hello World
  2. Name it zoho-contact-proxy and click Deploy
  3. Click Edit Code, delete the default code, paste the Worker code below, click Deploy
const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';

export default {
  async fetch(request, env) {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    const referer = request.headers.get('Referer') || '/';

    let formData;
    try {
      formData = await request.formData();
    } catch {
      return errorRedirect(referer, 'invalid_request');
    }

    const token = formData.get('cf-turnstile-response');
    if (!token) return errorRedirect(referer, 'missing_token');

    const verified = await verifyTurnstile(
      token,
      request.headers.get('CF-Connecting-IP'),
      env.TURNSTILE_SECRET_KEY
    );

    if (!verified) return errorRedirect(referer, 'token_invalid');

    formData.delete('cf-turnstile-response');

    try {
      const zohoRes = await fetch(env.ZOHO_FORM_URL, {
        method: 'POST',
        body: formData,
        redirect: 'follow',
      });
      if (!zohoRes.ok) return errorRedirect(referer, 'submission_failed');
    } catch {
      return errorRedirect(referer, 'submission_failed');
    }

    const successUrl = new URL(referer);
    successUrl.searchParams.set('zf_status', 'success');
    return Response.redirect(successUrl.toString(), 302);
  }
};

async function verifyTurnstile(token, ip, secret) {
  try {
    const res = await fetch(TURNSTILE_VERIFY_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ secret, response: token, remoteip: ip }),
    });
    const data = await res.json();
    return data.success === true;
  } catch {
    return false;
  }
}

function errorRedirect(referer, reason) {
  const url = new URL(referer);
  url.searchParams.set('zf_status', 'error');
  url.searchParams.set('zf_error', reason);
  return Response.redirect(url.toString(), 302);
}

4b — Add environment variables

In the Worker → Settings → Variables and Secrets:

Name Type Value
ZOHO_FORM_URL Text Your Zoho form htmlRecords/submit URL
TURNSTILE_SECRET_KEY Secret Your Turnstile Secret Key

⚠️ Always add TURNSTILE_SECRET_KEY as type Secret, never Text. Secrets are encrypted and never visible after saving.

4c — Point the form at the Worker

In index.html, change the form action from the Zoho URL to your Worker URL:

<form
  action="https://zoho-contact-proxy.yourname.workers.dev"
  method="POST"
  enctype="multipart/form-data"
  ...
>

4d — Pass verifyUrl to the init call

ZohoTurnstile.init({
  siteKey:   'YOUR_SITE_KEY',
  verifyUrl: 'https://zoho-contact-proxy.yourname.workers.dev'
});

Options reference

Option Type Required Default Description
siteKey string ✅ — Your Cloudflare Turnstile Site Key
verifyUrl string — null Worker base URL. Enables full server-side verification
theme string — 'light' 'light'
size string — 'normal' 'normal'
errorMessage string — 'Please complete the security check...' Shown when submission is blocked
containerId string — null Custom element ID to render the widget into
onSuccess function — null Fired when Turnstile issues a token. Receives the token as the first argument
onError function — null Fired when Turnstile encounters an error
onExpire function — null Fired when an issued token expires (tokens are valid for 5 minutes)

Methods

ZohoTurnstile.init(options)
Initialises the library. Injects the widget and patches the Zoho submit handler. Call once after the DOM is ready.

ZohoTurnstile.reset()
Resets the widget and issues a new challenge. Call this after a failed submission.

ZohoTurnstile.getToken()
Returns the current token string, or empty string if not yet issued. Useful for debugging.

Testing

Cloudflare provides special site keys for development:

Behaviour Site Key
Always passes 1x00000000000000000000AA
Always blocks 2x00000000000000000000AB
Forces interactive challenge 3x00000000000000000000FF

Use 3x00000000000000000000FF during development — it forces the checkbox interaction so you’re not silently pre-cleared on your own IP.

Verify the token is being issued — after solving the challenge, open DevTools → Elements and look inside the form for:

<input type="hidden" name="cf-turnstile-response" value="eyJ...">

Or run in the console:

ZohoTurnstile.getToken() // should return a non-empty string

Verify server-side is working — submit the form and check the redirect URL. You should land back on the page with ?zf_status=success.

Troubleshooting

Widget doesn’t appear
Check the browser console for [ZohoTurnstile] warnings. Confirm your domain is registered in the Turnstile dashboard and that zoho-turnstile.js loads before the init call.

Form submits without solving the challenge
You’re being silently pre-cleared — Cloudflare automatically passes visitors it already trusts. This is expected. Use test key 3x00000000000000000000FF to force an interactive challenge.

?zf_status=error&zf_error=token_invalid
The Worker received the submission but Cloudflare rejected the token. Confirm TURNSTILE_SECRET_KEY in the Worker matches the Secret Key (not Site Key) from the dashboard. Tokens also expire after 5 minutes — have the user refresh and resubmit.

?zf_status=error&zf_error=submission_failed
Token verified but the Zoho POST failed. Confirm ZOHO_FORM_URL is the full htmlRecords/submit URL from your form export. Check Worker logs under Workers → your worker → Logs.

How to Separate Delivery Failures from Execution Failures in Cron Jobs

TL;DR

When your cron job reports “failed,” it might have executed perfectly — only the notification delivery (Slack, email, webhook) failed. Monitoring execution and delivery as separate layers prevents false alarms and unnecessary re-runs.

The Problem: 9 “Broken” Jobs That Were Fine

I run 43 cron jobs on OpenClaw for content generation, analytics, and infrastructure tasks. One day, 9 jobs reported “Message failed” errors. Meanwhile, all 14 content generation jobs succeeded.

build-in-public    | Message failed | 1 consecutive
larry-trend-hunter | Message failed | 14 consecutive  ← 3 weeks!
app-metrics        | Message failed | 7 consecutive

My first instinct: “9 jobs are broken, fix them.” But when I investigated, every job had completed its actual work. Only the Slack notification step was failing.

Why This Happens

A typical cron job flow:

[Execute job] → [Generate output] → [Send notification] → [Report status]

“Message failed” occurs at step 3. But most cron schedulers report the final step’s result as the job’s status. So a delivery failure becomes a “job failure.”

This conflation causes two problems:

  1. False alarms: You investigate jobs that are working fine
  2. Missed fixes: The real issue (delivery infrastructure) gets buried under “job failure” noise

The Fix: Three-Layer Monitoring

Layer 1: Execution (Did the job run?)

RESULT=$(run_job 2>&1)
EXIT_CODE=$?
echo "{"job":"$JOB_NAME","exit_code":$EXIT_CODE,"ts":"$(date -u +%FT%TZ)"}" 
  >> /var/log/cron-execution.jsonl

Layer 2: Artifact (Did it produce the expected output?)

EXPECTED="/workspace/output/${TODAY}/result.json"
if [ -f "$EXPECTED" ]; then
  ARTIFACT_STATUS="success"
else
  ARTIFACT_STATUS="missing"
fi

Layer 3: Delivery (Did the notification reach its destination?)

HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" 
  -X POST "$SLACK_WEBHOOK" 
  -H "Content-Type: application/json" 
  -d "{"text":"$MSG"}")

if [ "$HTTP_CODE" != "200" ]; then
  # Log delivery failure separately — do NOT mark the job as failed
  echo "{"job":"$JOB_NAME","delivery":"failed","http":$HTTP_CODE}" 
    >> /var/log/cron-delivery.jsonl
fi

Alert Routing by Layer

Layer On Failure Severity
Layer 1 (Execution) Alert immediately, consider re-run 🔴 High
Layer 2 (Artifact) Alert, inspect logs 🟡 Medium
Layer 3 (Delivery) Retry delivery only. Do NOT re-run the job 🟢 Low

Results After Separation

Metric Before After
“Failed” jobs per day 9 0 (execution failures)
False alarms per day 9 0
Delivery issues detected Unknown (mixed in) 9 (isolated as Slack layer problem)

The trend-hunter job with 14 consecutive “errors” had been executing correctly every single time. A structural issue in the Slack delivery layer had gone unnoticed for 3 weeks because it was classified as a “job failure.”

Key Takeaways

Lesson Detail
Decompose “failure” Monitor execution, artifact, and delivery as independent layers
Delivery failure ≠ execution failure Your job may have succeeded even when no notification arrives
Look at the pattern of consecutive errors 14 consecutive identical errors point to infrastructure, not the job
Define re-run criteria Only re-run on Layer 1 failure. Re-running for Layer 3 failure wastes resources