added on local at 2026-07-01 21:47:07
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # RePricer - landing page + small marketing pages. | |
| 4 | # | |
| 5 | # Dispatches on SCRIPT_NAME. index.cgi is the landing page; the small | |
| 6 | # marketing pages (about/contact/faq/features/help/privacy/terms/why) | |
| 7 | # have been folded in as separate branches so we cut file count without | |
| 8 | # losing behavior. | |
| 9 | # | |
| 10 | # Bigger marketing pages (tour, tutorials, pricing, changelog, refer) | |
| 11 | # stay as their own scripts. | |
| 12 | # | |
| 13 | # Pricing block on the landing page pulls from billing_plans so it stays | |
| 14 | # in lockstep with /pricing.cgi and the admin Packages console. | |
| 15 | # | |
| 16 | # Consolidated: former /about.cgi, /contact.cgi, /faq.cgi, /features.cgi, | |
| 17 | # /help.cgi, /privacy.cgi, /terms.cgi, /why.cgi are now wrappers that do | |
| 18 | # this file; SCRIPT_NAME routes execution. | |
| 19 | #====================================================================== | |
| 20 | use strict; | |
| 21 | use warnings; | |
| 22 | ||
| 23 | use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com'; | |
| 24 | use CGI; | |
| 25 | use MODS::Template; | |
| 26 | use MODS::Login; | |
| 27 | use MODS::RePricer::Wrapper; | |
| 28 | ||
| 29 | $|=1; | |
| 30 | ||
| 31 | my $auth = MODS::Login->new; | |
| 32 | my $wrap = MODS::RePricer::Wrapper->new; | |
| 33 | my $tfile = MODS::Template->new; | |
| 34 | ||
| 35 | my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'index'; | |
| 36 | ||
| 37 | # Simple template-only pages, keyed by entry filename. | |
| 38 | # template = TEMPLATES/<file>.html | |
| 39 | # title = <title> for the wrapper | |
| 40 | # require_login = 1 -> bounce to /login.cgi if unauthenticated | |
| 41 | my %SIMPLE = ( | |
| 42 | about => { template => 'repricer_about.html', title => 'About' }, | |
| 43 | contact => { template => 'repricer_contact.html', title => 'Contact' }, | |
| 44 | features => { template => 'repricer_features.html', title => 'Features' }, | |
| 45 | why => { template => 'repricer_why.html', title => 'Why RePricer' }, | |
| 46 | help => { template => 'repricer_help.html', title => 'Help', require_login => 1, page_key => '' }, | |
| 47 | ); | |
| 48 | ||
| 49 | my $userinfo = $auth->login_verify(); # may be undef on marketing pages | |
| 50 | ||
| 51 | if ($SIMPLE{$entry}) { _mkt_simple($entry); exit; } | |
| 52 | if ($entry eq 'faq') { _mkt_faq(); exit; } | |
| 53 | if ($entry eq 'privacy') { _mkt_privacy(); exit; } | |
| 54 | if ($entry eq 'terms') { _mkt_terms(); exit; } | |
| 55 | ||
| 56 | # Default: landing page | |
| 57 | _mkt_landing(); | |
| 58 | exit; | |
| 59 | ||
| 60 | #====================================================================== | |
| 61 | # Landing page (index.cgi) | |
| 62 | #====================================================================== | |
| 63 | sub _mkt_landing { | |
| 64 | require MODS::DBConnect; | |
| 65 | require MODS::RePricer::Config; | |
| 66 | require MODS::RePricer::Billing; | |
| 67 | require MODS::RePricer::Promotions; | |
| 68 | ||
| 69 | my $db = MODS::DBConnect->new; | |
| 70 | my $cfg = MODS::RePricer::Config->new; | |
| 71 | my $bill = MODS::RePricer::Billing->new; | |
| 72 | my $DB = $cfg->settings('database_name'); | |
| 73 | ||
| 74 | my $dbh = $db->db_connect(); | |
| 75 | my @plans = _mkt_build_landing_plan_cards($db, $dbh, $DB, $bill); | |
| 76 | ||
| 77 | my $promo_obj = MODS::RePricer::Promotions->new; | |
| 78 | my $active_promo = $promo_obj->active_plan_promo($db, $dbh, $DB); | |
| 79 | my $promo_code = ($active_promo && $active_promo->{code}) ? $active_promo->{code} : ''; | |
| 80 | my $promo_label = $active_promo ? $promo_obj->marketing_label($active_promo) : ''; | |
| 81 | ||
| 82 | if ($active_promo) { | |
| 83 | my @eligible = $promo_obj->eligible_plan_ids($db, $dbh, $DB, $active_promo->{id}); | |
| 84 | my %eligible_set = map { $_ => 1 } @eligible; | |
| 85 | my $all_paid = !scalar(@eligible); | |
| 86 | foreach my $c (@plans) { | |
| 87 | next if ($c->{price_display} || '$0') eq '$0'; | |
| 88 | next unless $all_paid || $eligible_set{ $c->{id} || 0 }; | |
| 89 | $c->{promo_active} = 1; | |
| 90 | $c->{promo_code} = $promo_code; | |
| 91 | $c->{promo_label} = $promo_label; | |
| 92 | $c->{promo_card_line} = $promo_label . ' with code <strong>' . $promo_code . '</strong>'; | |
| 93 | } | |
| 94 | } | |
| 95 | ||
| 96 | $db->db_disconnect($dbh); | |
| 97 | ||
| 98 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n"; | |
| 99 | ||
| 100 | my $tvars = { | |
| 101 | user_id => $userinfo ? $userinfo->{user_id} : '', | |
| 102 | plans => \@plans, | |
| 103 | has_plans => scalar(@plans) ? 1 : 0, | |
| 104 | has_promo => $active_promo ? 1 : 0, | |
| 105 | promo_code => $promo_code, | |
| 106 | promo_label => $promo_label, | |
| 107 | }; | |
| 108 | ||
| 109 | my $body = join('', $tfile->template('repricer_index.html', $tvars, $userinfo)); | |
| 110 | ||
| 111 | $wrap->render({ | |
| 112 | userinfo => $userinfo, | |
| 113 | title => 'Reprice your catalog automatically while you sleep', | |
| 114 | body => $body, | |
| 115 | }); | |
| 116 | } | |
| 117 | ||
| 118 | #---------------------------------------------------------------------- | |
| 119 | sub _mkt_build_landing_plan_cards { | |
| 120 | my ($db, $dbh, $DB, $bill) = @_; | |
| 121 | return _mkt_landing_fallback_plans() unless $bill->schema_ready($db, $dbh, $DB); | |
| 122 | ||
| 123 | my @rows = $bill->list_plans($db, $dbh, $DB); | |
| 124 | return _mkt_landing_fallback_plans() unless scalar @rows; | |
| 125 | ||
| 126 | my $entitle_ready = $bill->plan_features_ready($db, $dbh, $DB); | |
| 127 | my @catalog = $bill->feature_catalog; | |
| 128 | ||
| 129 | my %by_tier; | |
| 130 | foreach my $r (@rows) { push @{ $by_tier{ $r->{tier} } }, $r; } | |
| 131 | ||
| 132 | my @tiers_seen; | |
| 133 | foreach my $r (@rows) { | |
| 134 | next if grep { $_ eq $r->{tier} } @tiers_seen; | |
| 135 | push @tiers_seen, $r->{tier}; | |
| 136 | } | |
| 137 | ||
| 138 | my @out; | |
| 139 | foreach my $tier (@tiers_seen) { | |
| 140 | my $monthly = (grep { ($_->{cadence}||'') eq 'monthly' } @{ $by_tier{$tier} })[0] | |
| 141 | || $by_tier{$tier}->[0]; | |
| 142 | next unless $monthly; | |
| 143 | ||
| 144 | my @bullets = _mkt_parse_features_for_landing($monthly->{features}); | |
| 145 | if (!scalar @bullets && $entitle_ready) { | |
| 146 | my $set = $bill->plan_feature_set($db, $dbh, $DB, $monthly->{id}); | |
| 147 | foreach my $f (@catalog) { | |
| 148 | next unless $set->{ $f->{key} }; | |
| 149 | push @bullets, $f->{name} . ' — ' . $f->{blurb}; | |
| 150 | } | |
| 151 | } | |
| 152 | @bullets = @bullets[0 .. ( $#bullets > 4 ? 4 : $#bullets )]; | |
| 153 | my $bullets_html = ''; | |
| 154 | foreach my $b (@bullets) { | |
| 155 | $bullets_html .= '<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">' | |
| 156 | . '<polyline points="20 6 9 17 4 12"/></svg> ' . $b . '</li>'; | |
| 157 | } | |
| 158 | ||
| 159 | my $tier_slug = lc($monthly->{tier} || ''); $tier_slug =~ s/[^a-z0-9_\-]//g; | |
| 160 | ||
| 161 | push @out, { | |
| 162 | id => $monthly->{id}, | |
| 163 | tier => $monthly->{tier}, | |
| 164 | display_name => $monthly->{display_name} || ucfirst($monthly->{tier} || ''), | |
| 165 | price_display => _mkt_fmt_price_short($monthly->{price_cents}), | |
| 166 | is_featured => $monthly->{is_featured} ? 1 : 0, | |
| 167 | not_featured => $monthly->{is_featured} ? 0 : 1, | |
| 168 | tagline => $monthly->{tagline} || '', | |
| 169 | has_tagline => $monthly->{tagline} ? 1 : 0, | |
| 170 | cta_label => $monthly->{cta_label} || ($monthly->{price_cents} ? 'Start trial' : 'Start free'), | |
| 171 | cta_href => $monthly->{price_cents} ? ('signup.cgi?plan=' . $tier_slug) : 'signup.cgi', | |
| 172 | bullets_html => $bullets_html, | |
| 173 | }; | |
| 174 | } | |
| 175 | return @out; | |
| 176 | } | |
| 177 | ||
| 178 | sub _mkt_fmt_price_short { | |
| 179 | my ($cents) = @_; | |
| 180 | $cents = 0 unless defined $cents; | |
| 181 | if ($cents % 100 == 0) { | |
| 182 | return '$' . int($cents / 100); | |
| 183 | } | |
| 184 | return '$' . sprintf('%.2f', $cents / 100); | |
| 185 | } | |
| 186 | ||
| 187 | sub _mkt_parse_features_for_landing { | |
| 188 | my ($raw) = @_; | |
| 189 | return () unless defined $raw && $raw =~ /\S/; | |
| 190 | my @out; | |
| 191 | while ($raw =~ m/\{\s*"name"\s*:\s*"((?:\\.|[^"\\])*)"\s*,\s*"included"\s*:\s*(true|false|0|1)\s*\}/gi) { | |
| 192 | my $name = $1; my $inc = lc $2; | |
| 193 | $name =~ s/\\"/"/g; $name =~ s/\\\\/\\/g; | |
| 194 | push @out, $name if $inc eq 'true' || $inc eq '1'; | |
| 195 | } | |
| 196 | return @out; | |
| 197 | } | |
| 198 | ||
| 199 | sub _mkt_landing_fallback_plans { | |
| 200 | my $li = sub { | |
| 201 | my @items = @_; | |
| 202 | my $h = ''; | |
| 203 | foreach my $t (@items) { | |
| 204 | $h .= '<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">' | |
| 205 | . '<polyline points="20 6 9 17 4 12"/></svg> ' . $t . '</li>'; | |
| 206 | } | |
| 207 | return $h; | |
| 208 | }; | |
| 209 | return ( | |
| 210 | { tier=>'pro', display_name=>'Pro', price_display=>'$49', | |
| 211 | is_featured=>1, not_featured=>0, | |
| 212 | tagline=>'Defend Buy Box on Amazon.', | |
| 213 | has_tagline=>1, cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=pro', | |
| 214 | bullets_html => $li->('Up to 1,000 SKUs', 'Reprice every 5 minutes', 'Amazon marketplace', 'All 8 strategies + MAP guardrails', 'Full audit log') }, | |
| 215 | { tier=>'business', display_name=>'Business', price_display=>'$149', | |
| 216 | is_featured=>0, not_featured=>1, | |
| 217 | tagline=>'Multi-marketplace, full automation.', | |
| 218 | has_tagline=>1, cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=business', | |
| 219 | bullets_html => $li->('Everything in Pro', 'Up to 10,000 SKUs', 'Reprice every minute', 'Amazon + Walmart', 'Audit log CSV export', 'Priority support') }, | |
| 220 | { tier=>'scale', display_name=>'Scale', price_display=>'$349', | |
| 221 | is_featured=>0, not_featured=>1, | |
| 222 | tagline=>'Enterprise scale, custom strategies, SLA.', | |
| 223 | has_tagline=>1, cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=scale', | |
| 224 | bullets_html => $li->('Everything in Business', 'Unlimited SKUs', 'Amazon + Walmart + eBay', 'Custom strategies', '99.95% uptime SLA', 'Dedicated CSM + SAML SSO') }, | |
| 225 | ); | |
| 226 | } | |
| 227 | ||
| 228 | #====================================================================== | |
| 229 | # Simple template-driven marketing pages | |
| 230 | #====================================================================== | |
| 231 | sub _mkt_simple { | |
| 232 | my ($key) = @_; | |
| 233 | my $spec = $SIMPLE{$key}; | |
| 234 | ||
| 235 | if ($spec->{require_login} && !$userinfo) { | |
| 236 | print "Status: 302 Found\nLocation: /login.cgi\n\n"; | |
| 237 | return; | |
| 238 | } | |
| 239 | ||
| 240 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n"; | |
| 241 | my $body = join('', $tfile->template($spec->{template}, {}, $userinfo)); | |
| 242 | my %args = ( | |
| 243 | userinfo => $userinfo, | |
| 244 | title => $spec->{title}, | |
| 245 | body => $body, | |
| 246 | ); | |
| 247 | $args{page_key} = $spec->{page_key} if defined $spec->{page_key}; | |
| 248 | $wrap->render(\%args); | |
| 249 | } | |
| 250 | ||
| 251 | #====================================================================== | |
| 252 | # FAQ (inline HTML -- template engine hangs on this content) | |
| 253 | #====================================================================== | |
| 254 | sub _mkt_faq { | |
| 255 | my @FAQ = ( | |
| 256 | { q => 'What does RePricer do?', | |
| 257 | a => 'It automatically watches your Amazon, eBay, and Walmart listings, sees what the competing sellers are charging, and adjusts your price to win the Buy Box (or whatever target you choose) without ever dropping below the floor you set.' }, | |
| 258 | { q => 'Which marketplaces are supported?', | |
| 259 | a => 'Amazon (SP-API, all 10 regions), eBay (Sell + Browse API, all major marketplaces), and Walmart (Marketplace API, US/Canada/Mexico). Each is wired through the official API -- no browser scraping, no rate-limit dodging.' }, | |
| 260 | { q => 'What is the Buy Box and why does it matter?', | |
| 261 | a => 'On Amazon, ~83% of all sales go through the Buy Box -- the "Add to Cart" button at the top of a product page. Walmart has a near-identical concept. Whichever seller is "winning the Buy Box" gets the lion\'s share of orders. The Buy Box winner is chosen by an algorithm that weighs price, shipping speed, seller rating, and stock. Price is the easiest of those for a small seller to control -- which is what RePricer automates.' }, | |
| 262 | { q => 'How does RePricer protect me from a race to the bottom?', | |
| 263 | a => 'Every product has a <strong>floor</strong> (defaults to your cost, configurable to anything you want). The repricer will never go below your floor, full stop -- even if it means giving up the Buy Box. You can also enforce a <strong>ceiling</strong> and a <strong>MAP</strong> (Minimum Advertised Price) for brand-restricted goods.' }, | |
| 264 | { q => 'What pricing strategies are available?', | |
| 265 | a => 'Eight built-in: <em>beat the lowest competitor</em>, <em>match the Buy Box</em>, <em>beat the Buy Box</em>, <em>target a specific search position</em>, <em>cost + markup</em> (defensive), <em>fixed price</em>, <em>Bayesian competitor score</em> (weights rivals by historical reliability), and <em>velocity-aware</em> (adjusts based on sell-through). You can apply rules per-account or per-product, with a priority system for when multiple rules match.' }, | |
| 266 | { q => 'How often does it reprice?', | |
| 267 | a => 'Pro: every 15 minutes. Business: every 5 minutes. Scale: every 1 minute. Each SKU also has a per-product cooldown so you don\'t hammer the same SKU 12 times an hour and get throttled by the marketplace.' }, | |
| 268 | { q => 'Does Amazon / eBay / Walmart penalize sellers who reprice this often?', | |
| 269 | a => 'No -- repricing is a first-class workflow on all three marketplaces, with API endpoints designed for exactly this. Their actual concern is API rate limits, which the cooldowns above are tuned to stay safely under.' }, | |
| 270 | { q => 'What happens if a competitor goes below my floor?', | |
| 271 | a => 'RePricer holds at your floor and you don\'t win the Buy Box that cycle. The next cycle, if they have raised again, you go back to beating them. Optional email alert: "Floor hit on SKU X" so you can decide whether to lower your floor or just leave the sale on the table.' }, | |
| 272 | { q => 'How do I connect my marketplace accounts?', | |
| 273 | a => 'Amazon and eBay use OAuth -- click "Connect" on the Marketplaces page, sign in on the marketplace\'s own consent screen, done. Walmart uses a Consumer ID + Private Key (Walmart\'s standard) that you paste in. We <em>never</em> store your marketplace login -- only the API tokens you delegate to us.' }, | |
| 274 | { q => 'Is RePricer secure?', | |
| 275 | a => 'Sign-in supports two-factor authentication (TOTP). All passwords are SHA-256 hashed. Marketplace OAuth tokens are stored encrypted at rest. Stripe handles all card data via Elements -- card numbers never touch our server. Audit log on every price change.' }, | |
| 276 | { q => 'How does billing work?', | |
| 277 | a => 'RePricer is a flat-fee SaaS subscription billed via Stripe. Monthly or annual (annual is roughly 2 months free). 14-day free trial on every plan, no card required to start. <strong>We never take a percentage of your marketplace revenue</strong> — your Amazon / Walmart / eBay payouts land directly in your seller-account bank, exactly as today. Cancel anytime; cancellations take effect at the end of your current cycle.' }, | |
| 278 | { q => 'What if my SKU isn\'t listed correctly on the marketplace?', | |
| 279 | a => 'RePricer only changes the <strong>price</strong> field. If the listing itself is wrong (title, image, category), you need to fix it on the marketplace directly -- our SP-API permissions intentionally don\'t include catalog editing.' }, | |
| 280 | { q => 'Can I see what changed and why?', | |
| 281 | a => 'Yes -- every price change is logged with the old price, new price, the rule that fired, the marketplace API response, and a timestamp. View on the per-product detail page or export the full log to CSV.' }, | |
| 282 | { q => 'What if Amazon rejects a price update?', | |
| 283 | a => 'It gets logged as <code>sync_status=failed</code> with the exact API error, and the worker retries on the next cycle. If a SKU keeps failing, it surfaces in the Errored Accounts panel on /admin_health.cgi.' }, | |
| 284 | { q => 'Can I pause repricing temporarily?', | |
| 285 | a => 'Yes -- "Pause all repricing" toggle on your dashboard. The worker skips your entire account until you flip it back. Useful during a Stripe billing issue, an Amazon account review, or a holiday weekend.' }, | |
| 286 | { q => 'Can my team have access too?', | |
| 287 | a => 'Business tier and up: up to 5 team seats with permission groups (manage billing, edit prices, view-only, etc.). Audit log records which team member made each change.' }, | |
| 288 | { q => 'Do you have an API?', | |
| 289 | a => 'Scale tier includes API access for custom integrations (read products, push prices, listen for events). Documented at /api_docs once your account is provisioned.' }, | |
| 290 | { q => 'What if I want to leave?', | |
| 291 | a => 'Cancel your subscription anytime. We keep your data for 90 days after cancellation in case you come back, then it\'s purged. CSV export of everything is one click before you go.' }, | |
| 292 | ); | |
| 293 | ||
| 294 | my $body = '<div style="max-width:780px;margin:0 auto;padding:24px 28px">'; | |
| 295 | $body .= ' <div style="font-size:11px;letter-spacing:2px;color:#14b8a6;text-transform:uppercase">Help center</div>'; | |
| 296 | $body .= ' <h1 style="font-size:30px;color:#fff;margin:6px 0 18px;font-weight:700">Frequently asked questions</h1>'; | |
| 297 | ||
| 298 | foreach my $f (@FAQ) { | |
| 299 | my $q_h = _mkt_h($f->{q}); | |
| 300 | my $a = $f->{a}; # answers can include simple HTML; not escaping | |
| 301 | $body .= qq~<details style="background:#0b1226;border:1px solid #1f2a4a;border-radius:10px;padding:14px 18px;margin-bottom:8px"> | |
| 302 | <summary style="cursor:pointer;color:#e6ecf6;font-size:15px;font-weight:600;list-style:none">$q_h</summary> | |
| 303 | <div style="color:#7e92b6;font-size:14px;line-height:1.6;margin-top:10px">$a</div> | |
| 304 | </details>~; | |
| 305 | } | |
| 306 | ||
| 307 | $body .= '<div style="margin-top:24px;padding:16px 20px;background:#0a0f1f;border:1px dashed #1f2a4a;border-radius:10px;text-align:center;color:#7e92b6;font-size:13px">'; | |
| 308 | $body .= 'Still have questions? Email <a href="mailto:support@repricer.3dshawn.com" style="color:#14b8a6">support@repricer.3dshawn.com</a>.'; | |
| 309 | $body .= '</div>'; | |
| 310 | $body .= '</div>'; | |
| 311 | ||
| 312 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n"; | |
| 313 | $wrap->render({ userinfo => $userinfo, title => 'FAQ', body => $body }); | |
| 314 | } | |
| 315 | ||
| 316 | sub _mkt_h { my $s = shift // ''; $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; $s =~ s/"/"/g; $s } | |
| 317 | ||
| 318 | #====================================================================== | |
| 319 | # Privacy policy (inline HTML) | |
| 320 | #====================================================================== | |
| 321 | sub _mkt_privacy { | |
| 322 | my $body = <<'HTML'; | |
| 323 | <div style="max-width:760px;margin:0 auto;padding:24px 28px;line-height:1.7;color:#e6ecf6"> | |
| 324 | <h1 style="font-size:28px;color:#fff;margin:0 0 6px">Privacy policy</h1> | |
| 325 | <p style="color:#7e92b6">Last updated: June 3, 2026</p> | |
| 326 | ||
| 327 | <h2 style="color:#14b8a6;margin-top:28px">TL;DR</h2> | |
| 328 | <p>We store the minimum data needed to reprice your marketplace listings. We never sell your data. We never look at your customer order data — our marketplace API permissions only request pricing and listing scopes. We use Stripe for billing; card numbers never touch our server.</p> | |
| 329 | ||
| 330 | <h2 style="color:#14b8a6;margin-top:28px">What we collect</h2> | |
| 331 | <ul> | |
| 332 | <li><b>Account info:</b> email, display name, password (hashed), timezone, currency preference.</li> | |
| 333 | <li><b>Marketplace OAuth tokens</b> (Amazon LWA refresh token, eBay OAuth2 refresh token, Walmart Consumer ID + Private Key). Used only to read your competitor offers and update your prices.</li> | |
| 334 | <li><b>Your product catalog:</b> SKU, ASIN/Item ID, title, cost, floor/ceiling, repricing rules.</li> | |
| 335 | <li><b>Pricing telemetry we generate:</b> every price change we make (audit log), competitor offer snapshots, Buy Box history.</li> | |
| 336 | <li><b>Billing data:</b> Stripe customer ID, subscription ID, invoice history. Card numbers stored on Stripe, not us.</li> | |
| 337 | <li><b>Login/security:</b> session cookies, IP + user agent of each login, 2FA secret if enabled.</li> | |
| 338 | </ul> | |
| 339 | ||
| 340 | <h2 style="color:#14b8a6;margin-top:28px">What we do NOT collect</h2> | |
| 341 | <ul> | |
| 342 | <li>Customer order data on your marketplace accounts (scope not requested).</li> | |
| 343 | <li>Any data from buyers of your products.</li> | |
| 344 | <li>Credit card numbers / CVV / bank details — Stripe handles all of those.</li> | |
| 345 | </ul> | |
| 346 | ||
| 347 | <h2 style="color:#14b8a6;margin-top:28px">Who we share with</h2> | |
| 348 | <ul> | |
| 349 | <li><b>Amazon / eBay / Walmart:</b> we call their APIs on your behalf using your delegated tokens.</li> | |
| 350 | <li><b>Stripe:</b> subscription billing only.</li> | |
| 351 | <li><b>SMTP / email provider</b> (your choice via /admin_config.cgi): outbound notification emails.</li> | |
| 352 | <li><b>That's it.</b> No ad trackers, no analytics SaaS, no "data partners."</li> | |
| 353 | </ul> | |
| 354 | ||
| 355 | <h2 style="color:#14b8a6;margin-top:28px">Your rights (GDPR / CCPA)</h2> | |
| 356 | <ul> | |
| 357 | <li><b>Export your data:</b> request a JSON dump at <a href="/account_export.cgi" style="color:#14b8a6">/account_export.cgi</a>.</li> | |
| 358 | <li><b>Delete your account:</b> request closure at <a href="/account_close.cgi" style="color:#14b8a6">/account_close.cgi</a>. 30-day grace window in case it's a mistake; after that everything is purged (Stripe invoice records retained for tax/legal reasons, 7 years).</li> | |
| 359 | <li><b>Correct your data:</b> edit at <a href="/profile.cgi" style="color:#14b8a6">/profile.cgi</a>.</li> | |
| 360 | <li><b>Revoke marketplace access:</b> click Disconnect on <a href="/marketplaces.cgi" style="color:#14b8a6">/marketplaces.cgi</a>; tokens wiped immediately.</li> | |
| 361 | </ul> | |
| 362 | ||
| 363 | <h2 style="color:#14b8a6;margin-top:28px">Security</h2> | |
| 364 | <p>Marketplace OAuth tokens encrypted at rest. Passwords SHA-256 hashed. 2FA available with backup codes. Sign-in sessions revocable individually at <a href="/sessions.cgi" style="color:#14b8a6">/sessions.cgi</a>.</p> | |
| 365 | ||
| 366 | <h2 style="color:#14b8a6;margin-top:28px">Cookies</h2> | |
| 367 | <p>We use one first-party cookie (<code>repricer_session</code>, set on sign-in). One additional cookie (<code>repricer_cookie_ok</code>) records that you dismissed the cookie banner. No third-party cookies, no advertising pixels.</p> | |
| 368 | ||
| 369 | <h2 style="color:#14b8a6;margin-top:28px">Children</h2> | |
| 370 | <p>RePricer is a B2B SaaS. We don't knowingly collect data from anyone under 16. Email <a href="mailto:privacy@repricer.3dshawn.com" style="color:#14b8a6">privacy@repricer.3dshawn.com</a> if you believe we've received data from a minor.</p> | |
| 371 | ||
| 372 | <h2 style="color:#14b8a6;margin-top:28px">Changes</h2> | |
| 373 | <p>Material changes are emailed to every active user and posted on <a href="/changelog.cgi" style="color:#14b8a6">/changelog.cgi</a>.</p> | |
| 374 | ||
| 375 | <h2 style="color:#14b8a6;margin-top:28px">Contact</h2> | |
| 376 | <p>Privacy questions: <a href="mailto:privacy@repricer.3dshawn.com" style="color:#14b8a6">privacy@repricer.3dshawn.com</a>.</p> | |
| 377 | </div> | |
| 378 | HTML | |
| 379 | ||
| 380 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n"; | |
| 381 | $wrap->render({ userinfo => $userinfo, title => 'Privacy policy', body => $body }); | |
| 382 | } | |
| 383 | ||
| 384 | #====================================================================== | |
| 385 | # Terms of service (inline HTML) | |
| 386 | #====================================================================== | |
| 387 | sub _mkt_terms { | |
| 388 | my $body = <<'HTML'; | |
| 389 | <div style="max-width:760px;margin:0 auto;padding:24px 28px;line-height:1.7;color:#e6ecf6"> | |
| 390 | <h1 style="font-size:28px;color:#fff;margin:0 0 6px">Terms of service</h1> | |
| 391 | <p style="color:#7e92b6">Last updated: June 21, 2026</p> | |
| 392 | ||
| 393 | <h2 style="color:#14b8a6;margin-top:28px">1. The deal</h2> | |
| 394 | <p>RePricer ("we") is a SaaS that reads your Amazon / Walmart / eBay marketplace listings and adjusts prices on your behalf, per rules and guardrails you configure. You ("the seller") sign up, pay a subscription, and grant us API access via OAuth or signed marketplace credentials. RePricer plans are <b>Pro</b>, <b>Business</b>, and <b>Scale</b>; there is no free tier — every plan begins with a 14-day free trial.</p> | |
| 395 | ||
| 396 | <h2 style="color:#14b8a6;margin-top:28px">2. Your account</h2> | |
| 397 | <p>You're responsible for actions on your RePricer account, including by team members you invite. Keep your password and 2FA secret safe. Team-seat sharing is allowed on Business and Scale; on Pro, each seat requires its own account. We may suspend accounts that violate these Terms, our acceptable-use policy, marketplace policies that apply to your seller account, or applicable law.</p> | |
| 398 | ||
| 399 | <h2 style="color:#14b8a6;margin-top:28px">3. The pricing engine</h2> | |
| 400 | <p>The repricer changes prices based on the rules you set. <b>You are responsible for those rules.</b> We provide hard floors, ceilings, and MAP enforcement so misconfigured rules don't cause infinite undercutting — but if you set a floor of $0.01 and someone undercuts you, you'll sell at $0.01.</p> | |
| 401 | <p>We are not responsible for losses from misconfigured rules, missed Buy Box opportunities, or competitor behavior. Marketplace policy violations (e.g. antitrust laws, brand MAP agreements) are your responsibility as the seller.</p> | |
| 402 | ||
| 403 | <h2 style="color:#14b8a6;margin-top:28px">4. Marketplace API permissions</h2> | |
| 404 | <p>You delegate API access via each marketplace's own OAuth (Amazon, eBay) or signed-credential (Walmart) grant flow. We use it only for pricing, listing-read, and competitor-offer scopes. We do not read customer order data beyond what is required to reconcile sell-through velocity. You can revoke any time via the marketplace's seller dashboard or our /marketplaces.cgi.</p> | |
| 405 | ||
| 406 | <h2 style="color:#14b8a6;margin-top:28px">5. Subscription billing & the SaaS relationship</h2> | |
| 407 | <p>RePricer is a software-as-a-service subscription. Your subscription fee is the only payment you make to us, billed monthly in advance via Stripe to the card on file. <b>RePricer does NOT process buyer transactions on the marketplaces you connect</b>; those run through Amazon's, Walmart's, and eBay's own payment systems, and you receive payouts directly from those marketplaces to your seller-account bank, exactly as you do today. RePricer is not the merchant of record for any marketplace sale, takes no percentage of your marketplace revenue, and is not in the payment path between buyer and seller.</p> | |
| 408 | <p>14-day free trial on every plan; no card required during trial. Email reminder before trial ends. Cancel any time at <a href="/billing.cgi" style="color:#14b8a6">/billing.cgi</a>; cancellations take effect at the end of the current cycle. Refunds at our discretion within 14 days of a renewal charge, issued as account credit on the next invoice by default.</p> | |
| 409 | ||
| 410 | <h2 style="color:#14b8a6;margin-top:28px">6. Acceptable use</h2> | |
| 411 | <ul> | |
| 412 | <li>Don't use RePricer to violate marketplace ToS, antitrust laws, or contracts (e.g. brand MAP agreements you are bound by).</li> | |
| 413 | <li>Don't reverse-engineer the platform.</li> | |
| 414 | <li>Don't scrape, abuse our API, or impersonate other users.</li> | |
| 415 | </ul> | |
| 416 | ||
| 417 | <h2 style="color:#14b8a6;margin-top:28px">7. Uptime & outages</h2> | |
| 418 | <p>We aim for high availability across all paid plans. The <b>Scale plan includes a 99.95% monthly uptime SLA with prorated service credits</b> when we miss it; Pro and Business have no formal SLA but share the same underlying infrastructure. Marketplace API outages are out of our control; the repricer skips and resumes on the next cycle.</p> | |
| 419 | ||
| 420 | <h2 style="color:#14b8a6;margin-top:28px">8. API rate limits & marketplace API costs</h2> | |
| 421 | <p>RePricer connects to Amazon SP-API, Walmart Marketplace API, and eBay Trading / REST APIs on your behalf. We comply with each marketplace's published rate-limit and acceptable-use policies. If your account's usage exceeds typical patterns for your plan — for example, reprice attempts above your tier's documented frequency or excessive competitor-poll volume — <b>we may queue or throttle excess calls</b> to stay within marketplace limits and protect your seller account from rate-limit warnings or suspension. We are not responsible for marketplace-side rate-limit or velocity actions that originate from non-RePricer traffic on the same seller account.</p> | |
| 422 | ||
| 423 | <h2 style="color:#14b8a6;margin-top:28px">9. Liability</h2> | |
| 424 | <p>To the maximum extent permitted by law, our total liability for any claim is capped at the amount you paid us in the 12 months before the claim. We are not liable for indirect, incidental, or consequential damages — including lost Buy Box revenue, lost margin, or marketplace seller-account suspensions resulting from rules you configured.</p> | |
| 425 | ||
| 426 | <h2 style="color:#14b8a6;margin-top:28px">10. Termination</h2> | |
| 427 | <p>Either party can terminate any time. 30 days' notice if we discontinue the service or your tier. Your data, rules, and audit-log archive are retained for 90 days after cancellation so you can re-activate or export; after that permanently purged (Stripe invoice records retained 7 years for tax compliance).</p> | |
| 428 | ||
| 429 | <h2 style="color:#14b8a6;margin-top:28px">11. Changes to these terms</h2> | |
| 430 | <p>Material changes are emailed to every active user and posted on <a href="/changelog.cgi" style="color:#14b8a6">/changelog.cgi</a> at least 30 days before they take effect. Continued use after the effective date = acceptance.</p> | |
| 431 | ||
| 432 | <h2 style="color:#14b8a6;margin-top:28px">12. Governing law</h2> | |
| 433 | <p>Governed by US laws (state of operation), excluding conflict-of-laws principles. Disputes go to good-faith arbitration first; if that fails, the courts of the state of operation have jurisdiction.</p> | |
| 434 | ||
| 435 | <h2 style="color:#14b8a6;margin-top:28px">13. Contact</h2> | |
| 436 | <p>Legal: <a href="mailto:legal@repricer.3dshawn.com" style="color:#14b8a6">legal@repricer.3dshawn.com</a>. Support: <a href="mailto:support@repricer.3dshawn.com" style="color:#14b8a6">support@repricer.3dshawn.com</a>.</p> | |
| 437 | </div> | |
| 438 | HTML | |
| 439 | ||
| 440 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n"; | |
| 441 | $wrap->render({ userinfo => $userinfo, title => 'Terms of service', body => $body }); | |
| 442 | } |