# Click attribution

## Event model

Every click on a Seekmodo-ranked result is mirrored to the gateway
asynchronously. The local copy is written to the
`{$wpdb->prefix}seekmodo_events` table so the visitor's request never
blocks on the gateway round-trip; the `seekmodo_wp_events_flush` cron
drains the table every minute.

Event payload (sent to `/v1/events`):

```json
{
  "type": "click",
  "tenant_id": "trek_ca",
  "keyword": "spruce lake",
  "object_id": 4321,
  "object_type": "post",
  "position": 3,
  "surface": "serp",
  "bot_verdict": "human",
  "session_id": "anon-hashed-ip+ua",
  "occurred_at_unix": 1717286412
}
```

`surface` values: `serp` (full-page search result), `typeahead`
(dropdown), `related` (related-posts widget, if wired), `block` (the
Seekmodo block on a non-search page).

`bot_verdict`: `human`, `bot`, or `null` (gateway will run server-side
classification if null).

## Theme wiring (manual)

Stock WordPress and most Woo themes are auto-instrumented: the plugin
enqueues `seekmodo-beacon.js` on search templates and adds the
required `data-seekmodo-*` attributes via `the_posts` filter (content)
or `woocommerce_loop_product_link` (commerce).

For custom themes that build their result rows from scratch, attach
the attributes yourself:

```php
echo sprintf(
    '<a href="%s" data-seekmodo-id="%d" data-seekmodo-pos="%d" data-seekmodo-surface="serp">%s</a>',
    esc_url(get_permalink()),
    get_the_ID(),
    $position_in_loop,
    esc_html(get_the_title())
);
```

The bundled beacon then handles the click POST without any extra JS.

## Server-side mirror

You can also mirror clicks from PHP (e.g. if you're routing clicks via
a tracking endpoint of your own):

```php
numinix_seekmodo_mirror_click(
    $keyword,      // string
    $object_id,    // int
    $position,     // int, 1-based
    'human',       // bot verdict (or null)
    ['surface' => 'serp']
);
```

The helper is a no-op when the connector is disabled, so callers don't
need to guard.
