*/ public static function signals( $order ) { $out = array(); $via = method_exists( $order, 'get_created_via' ) ? (string) $order->get_created_via() : ''; $source = trim( (string) $order->get_meta( '_wc_order_attribution_utm_source' ) ); $agent = self::agent_name( $source ); if ( 'stripe-agentic-commerce' === $via ) { $out[] = array( 'strength' => 'strong', 'agent' => '' !== $agent ? $agent : 'Agent checkout', 'detail' => 'Created by Stripe agentic checkout' . ( '' !== $agent ? ' for ' . $agent : '' ) . '.', ); } if ( '1' === (string) $order->get_meta( '_agentic_commerce' ) ) { $out[] = array( 'strength' => 'strong', 'agent' => '' !== $agent ? $agent : 'Agent checkout', 'detail' => 'Created by PayPal agentic checkout' . ( '' !== $agent ? ' for ' . $agent : '' ) . '.', ); } $named = trim( (string) $order->get_meta( '_agent_stamp_payment_agent' ) ); if ( '' !== $named ) { $out[] = array( 'strength' => 'strong', 'agent' => self::agent_name( $named ), 'detail' => 'Stripe names the agent on the payment: ' . $named . '.', ); } if ( self::paid_with_link( $order ) ) { $out[] = array( 'strength' => 'weak', 'agent' => '', 'detail' => 'Paid with a Stripe Link wallet. Agents such as Muse and Grok Bot pay this way, and so do many people.', ); } return $out; } /** * @param object $order * @return bool */ private static function paid_with_link( $order ) { if ( 'link' === strtolower( (string) $order->get_meta( '_stripe_upe_payment_type' ) ) ) { return true; } $details = json_decode( (string) $order->get_meta( '_wcpay_payment_method_details' ), true ); if ( ! is_array( $details ) ) { return false; } if ( isset( $details['type'] ) && 'link' === $details['type'] ) { return true; } return isset( $details['card']['wallet']['type'] ) && 'link' === $details['card']['wallet']['type']; } /** * A display name for what the payment side calls the agent. * * @param string $raw * @return string */ public static function agent_name( $raw ) { $raw = trim( (string) $raw ); $known = array( 'chatgpt' => 'ChatGPT', 'openai' => 'ChatGPT', 'perplexity' => 'Perplexity', 'gemini' => 'Gemini', 'google' => 'Google', 'copilot' => 'Copilot', 'microsoft' => 'Copilot', 'muse' => 'Muse', 'meta' => 'Muse', 'grok' => 'Grok', 'xai' => 'Grok', 'claude' => 'Claude', 'anthropic' => 'Claude', 'instinct' => 'Instinct', ); $lower = strtolower( $raw ); foreach ( $known as $needle => $name ) { if ( false !== strpos( $lower, $needle ) ) { return $name; } } return preg_match( '/^[A-Za-z][A-Za-z0-9 ._-]{0,39}$/', $raw ) ? $raw : ''; } /** * The stamp after a strong payment signal. A verified signature keeps its stamp. * * @param array{verdict:string,name:string,confidence:string,detail:string} $result * @param array $signals * @return array{verdict:string,name:string,confidence:string,detail:string} */ public static function restamp( array $result, array $signals ) { $strong = null; foreach ( $signals as $signal ) { if ( 'strong' === $signal['strength'] ) { $strong = $signal; break; } } if ( null === $strong || ( 'agent' === $result['verdict'] && 'signed' === $result['confidence'] ) ) { return $result; } return array( 'verdict' => 'agent', 'name' => $strong['agent'], 'confidence' => 'payment', 'detail' => trim( $strong['detail'] . ' Before the payment the order was stamped ' . AgentStamp_Classifier::label( $result ) . '.' ), ); } public static function init() { add_action( 'woocommerce_order_status_changed', array( __CLASS__, 'paid' ), 5, 4 ); add_action( self::LOOKUP_HOOK, array( __CLASS__, 'lookup' ) ); } /** * When an order is paid, read what the payment says, before the funnel records it. * * @param int $order_id * @param string $from * @param string $to * @param WC_Order $order */ public static function paid( $order_id, $from, $to, $order = null ) { if ( ! is_object( $order ) || ! in_array( $to, array( 'processing', 'completed', 'on-hold' ), true ) ) { return; } self::apply( $order ); if ( 0 === strpos( (string) $order->get_payment_method(), 'stripe' ) && '' !== (string) $order->get_meta( '_stripe_intent_id' ) && '' === (string) $order->get_meta( '_agent_stamp_payment_looked' ) && function_exists( 'as_enqueue_async_action' ) ) { $order->update_meta_data( '_agent_stamp_payment_looked', '1' ); $order->save_meta_data(); as_enqueue_async_action( self::LOOKUP_HOOK, array( (int) $order_id ), 'agent-stamp' ); } } /** * @param WC_Order $order */ public static function apply( $order ) { $signals = self::signals( $order ); if ( empty( $signals ) ) { return; } $order->update_meta_data( '_agent_stamp_payment', wp_json_encode( $signals ) ); $current = array( 'verdict' => (string) $order->get_meta( '_agent_stamp_verdict' ), 'name' => (string) $order->get_meta( '_agent_stamp_name' ), 'confidence' => (string) $order->get_meta( '_agent_stamp_confidence' ), 'detail' => (string) $order->get_meta( '_agent_stamp_detail' ), ); if ( '' === $current['verdict'] ) { $current = array( 'verdict' => 'unknown', 'name' => '', 'confidence' => 'none', 'detail' => '', ); } $next = self::restamp( $current, $signals ); if ( $next !== $current && 'payment' !== $current['confidence'] ) { $order->update_meta_data( '_agent_stamp_verdict', $next['verdict'] ); $order->update_meta_data( '_agent_stamp_name', $next['name'] ); $order->update_meta_data( '_agent_stamp_confidence', $next['confidence'] ); $order->update_meta_data( '_agent_stamp_detail', $next['detail'] ); // An order an agent checkout created never visited the store, so it is its own session in the funnel. if ( '' === (string) $order->get_meta( '_agent_stamp_session' ) ) { $order->update_meta_data( '_agent_stamp_session', substr( hash( 'sha256', 'order|' . $order->get_id() ), 0, 16 ) ); } $order->update_meta_data( '_agent_stamp_kind', 'payment' ); $order->update_meta_data( '_agent_stamp_agent', AgentStamp_Classifier::label( $next ) ); $order->add_order_note( sprintf( /* translators: %s: stamp */ __( 'Agent Stamp: the payment says an agent placed this order. Stamp is now %s.', 'agent-stamp' ), AgentStamp_Classifier::label( $next ) ) ); } $order->save_meta_data(); } /** * Ask Stripe whether it named an agent on this payment. * * agent_details is in Stripe's preview API (private preview in 2026). The call uses * the Stripe gateway's own key and API class; when the account or the version does * not allow it, the lookups pause for a day and nothing changes. * * @param int $order_id */ public static function lookup( $order_id ) { if ( ! class_exists( 'WC_Stripe_API' ) || false !== get_transient( 'agent_stamp_payment_lookup_off' ) ) { return; } $order = wc_get_order( (int) $order_id ); $intent = $order ? (string) $order->get_meta( '_stripe_intent_id' ) : ''; if ( '' === $intent || ! preg_match( '/^pi_[A-Za-z0-9]+$/', $intent ) ) { return; } $version = defined( 'WC_Stripe_API::AGENTIC_COMMERCE_API_VERSION' ) ? constant( 'WC_Stripe_API::AGENTIC_COMMERCE_API_VERSION' ) : ''; if ( '' === $version ) { set_transient( 'agent_stamp_payment_lookup_off', 1, DAY_IN_SECONDS ); return; } $header = static function ( $headers ) use ( $version ) { $headers['Stripe-Version'] = $version; return $headers; }; add_filter( 'wc_stripe_request_headers', $header ); $response = WC_Stripe_API::retrieve( 'payment_intents/' . $intent . '?expand[]=agent_details' ); remove_filter( 'wc_stripe_request_headers', $header ); if ( is_wp_error( $response ) || ! is_object( $response ) || ! empty( $response->error ) ) { set_transient( 'agent_stamp_payment_lookup_off', 1, DAY_IN_SECONDS ); return; } $details = isset( $response->agent_details ) && is_object( $response->agent_details ) ? $response->agent_details : null; $name = ''; foreach ( array( 'name', 'display_name' ) as $field ) { if ( $details && isset( $details->$field ) && is_string( $details->$field ) && '' !== trim( $details->$field ) ) { $name = trim( $details->$field ); break; } } if ( '' === $name ) { return; } $order->update_meta_data( '_agent_stamp_payment_agent', substr( sanitize_text_field( $name ), 0, 60 ) ); $order->save_meta_data(); self::apply( $order ); } /** * Lines for the order box. * * @param WC_Order $order * @return string[] */ public static function evidence_lines( $order ) { $signals = json_decode( (string) $order->get_meta( '_agent_stamp_payment' ), true ); $lines = array(); foreach ( is_array( $signals ) ? $signals : array() as $signal ) { if ( isset( $signal['detail'] ) ) { $lines[] = (string) $signal['detail']; } } return $lines; } }