The canonical feed record, per-marketplace field mappers, and async push orchestration. The pattern that makes adding a fifth marketplace a 30-minute task.
Pushing inventory to marketplaces sounds simple until you try to do it for more than one. Boat Trader expects one field name. YachtWorld expects a different one. BoatUS wants the price formatted differently. Without a canonical layer in the middle, each marketplace becomes its own implementation with its own error handling and its own drift path. With one, the marketplaces are interchangeable.
The Anchor site had inventory data in WordPress custom post meta. The marketplaces each had their own APIs with different field names, different authentication schemes, and different required vs. optional fields. A naive implementation would have had one push function per marketplace, each reading directly from post meta and mapping fields inline. Every bug fix, every field addition, would require touching all four functions. Adding a fifth marketplace would mean writing a fifth parallel implementation.
The feed architecture solves this with a canonical intermediate layer: a structured array that all four marketplace adapters read from, and a single function that builds it from post meta.
Every push starts by building a canonical record from the unit’s post meta. This is the single source of truth that all marketplace adapters consume:
function [client]_build_feed_record( int $unit_id ): array {
return [
'sku' => get_post_meta( $unit_id, '[client]_stock_number', true ),
'title' => get_the_title( $unit_id ),
'description' => get_post_meta( $unit_id, '[client]_ai_description', true )
?: get_post_meta( $unit_id, '[client]_description', true ),
'price' => (int) get_post_meta( $unit_id, '[client]_price', true ),
'year' => (int) get_post_meta( $unit_id, '[client]_year', true ),
'make' => get_post_meta( $unit_id, '[client]_make', true ),
'model' => get_post_meta( $unit_id, '[client]_model', true ),
'length_ft' => (float) get_post_meta( $unit_id, '[client]_length', true ),
'hours' => (int) get_post_meta( $unit_id, '[client]_hours', true ),
'condition' => get_post_meta( $unit_id, '[client]_condition', true ),
'category' => get_post_meta( $unit_id, '[client]_category', true ),
'engine' => get_post_meta( $unit_id, '[client]_engine', true ),
'fuel_type' => get_post_meta( $unit_id, '[client]_fuel_type', true ),
'hull_type' => get_post_meta( $unit_id, '[client]_hull_type', true ),
'photos' => [client]_get_unit_photo_urls( $unit_id ),
'url' => get_permalink( $unit_id ),
'status' => get_post_meta( $unit_id, '[client]_status', true ),
'updated_at' => current_time( 'mysql' ),
];
}
Before any marketplace adapter runs, the record is validated. A unit with a missing required field doesn’t trigger an API call — it gets flagged and skipped cleanly:
function [client]_validate_feed_record( array $record ): array {
$required = [ 'sku', 'title', 'price', 'year', 'make', 'model', 'condition' ];
$issues = [];
foreach ( $required as $field ) {
if ( empty( $record[ $field ] ) ) {
$issues[] = "Missing required field: {$field}";
}
}
if ( $record['price'] < 500 || $record['price'] > 5000000 ) {
$issues[] = "Price out of range: {$record['price']}";
}
if ( empty( $record['photos'] ) ) {
$issues[] = 'No photos — most marketplaces require at least one';
}
return $issues;
}
Each marketplace gets its own mapper function. The mapper reads the canonical record and produces the payload shape that marketplace expects. Adding a new marketplace means adding one mapper, not touching anything else:
function [client]_map_to_boat_trader( array $record ): array {
return [
'ListingID' => $record['sku'],
'Title' => $record['title'],
'Description' => $record['description'],
'AskingPrice' => $record['price'],
'Year' => $record['year'],
'Make' => $record['make'],
'Model' => $record['model'],
'LengthFeet' => $record['length_ft'],
'HoursOnEngine' => $record['hours'],
'Condition' => ucfirst( $record['condition'] ),
'BoatType' => [client]_map_category_to_boat_trader( $record['category'] ),
'Images' => array_map( fn( $url ) => [ 'URL' => $url ], $record['photos'] ),
];
}
function [client]_map_to_yachtworld( array $record ): array {
return [
'listing_reference' => $record['sku'],
'headline' => $record['title'],
'description_text' => $record['description'],
'asking_price_usd' => $record['price'],
'model_year' => $record['year'],
'builder' => $record['make'],
'model_name' => $record['model'],
'loa_feet' => $record['length_ft'],
'engine_hours' => $record['hours'],
'listing_condition' => $record['condition'],
'media_urls' => $record['photos'],
];
}
The push is triggered on save_post but runs asynchronously via wp_schedule_single_event. This keeps the save action fast (no API timeout on the admin user’s request) and lets WP-Cron handle the actual push in the background:
add_action( 'save_post_[client]_unit', '[client]_queue_marketplace_sync', 10, 2 );
function [client]_queue_marketplace_sync( int $post_id, WP_Post $post ): void {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
// 5-second delay: lets the save complete before the cron fires
wp_schedule_single_event( time() + 5, '[client]_push_to_all_marketplaces', [ $post_id ] );
}
add_action( '[client]_push_to_all_marketplaces', '[client]_sync_to_marketplaces' );
function [client]_sync_to_marketplaces( int $unit_id ): void {
$record = [client]_build_feed_record( $unit_id );
$issues = [client]_validate_feed_record( $record );
if ( ! empty( $issues ) ) {
[client]_log_feed_push( $unit_id, 'all', 'validation_error', 0,
implode( '; ', $issues ) );
return;
}
$marketplaces = [client]_get_active_marketplaces();
foreach ( $marketplaces as $marketplace_id ) {
[client]_push_to_marketplace( $unit_id, $marketplace_id, $record );
}
}
The canonical record layer is what makes the system maintainable. When Boat Trader changes a field name in their API, one mapper function changes. When you add a new field to inventory (e.g., hull_color), you add it to [client]_build_feed_record() once and it’s available to all four marketplace mappers. When you add a fifth marketplace, you write one mapper and one push function — the orchestration doesn’t change.
Validation before the API call matters for cost and reliability. An API call to a marketplace for a unit with no photos wastes the request, uses API quota, and may generate a confusing error response. Better to catch the missing photo before making the call and surface it as a clear validation error.
4 marketplaces connected: Boat Trader, YachtWorld, BoatUS, and a regional aggregator. Average push latency from save_post to all four marketplaces updated: 12 seconds. Before this build, a unit marked sold on the website remained listed on the marketplaces for an average of 3.2 days. After: off-market across all platforms within 60 seconds of the status change.
Adding the fourth marketplace (the regional aggregator) required one mapper function, one push function, and adding a row to the active marketplaces config. Elapsed time: 28 minutes.
save_post means a staff member waiting 10 seconds for every save — and timing out when an API is unavailable.Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →