View difference between Paste ID: ingsarnd and xryuaiJR
SHOW: | | - or go back to the newest paste.
1
/*
2-
Polymarket Commission Email Filter - Google Apps Script
2+
Polymarket Commission Email Filter v2 - Google Apps Script
3
4-
Archives matching commission emails strictly below LIMIT_DOLLARS.
4+
5-
The default threshold is $10; change it to your preferred amount.
5+
1. Create a project at https://script.google.com/ and paste this entire file into Code.gs.
6-
Messages remain available in Gmail's All Mail.
6+
2. Set LIMIT_DOLLARS to your preferred threshold (default: $10).
7
3. In the editor, go to Services > + > Gmail API > Add.
8
4. Run archiveSmallCommissions once and authorize Gmail access.
9-
1. Create a project at https://script.google.com/ and paste this code into Code.gs.
9+
5. Add one hourly time-driven trigger for archiveSmallCommissions.
10-
2. Set LIMIT_DOLLARS to your preferred threshold.
10+
   If you already have this trigger, keep it instead of adding a duplicate.
11
6. For a standard Google Cloud project, also enable the Gmail API in its console.
12
13-
5. Go to Triggers > Add Trigger, select archiveSmallCommissions,
13+
BEHAVIOR
14-
   choose Time-driven, and set your preferred schedule.
14+
- Archives matching inbox messages with commissions strictly below the threshold.
15-
6. If you use a standard Google Cloud project, also enable the Gmail API
15+
- Reads the sender and subject; leaves message bodies unread by this script.
16-
   in that project's Google Cloud console.
16+
- Inspects at most 100 messages per run, with at least one second between API calls.
17
- Uses a hidden Gmail label to remember inspected messages, including ones kept
18
  in the inbox. Changing the threshold creates a new marker for future checks.
19
- Retries temporary API failures with increasing delays, then defers remaining
20
  work to the next scheduled run. Other errors remain visible as failures.
21
- Stops early to leave time before the Apps Script execution limit.
22
- To inspect a marked message again at the same threshold, remove its
23
  PolymarketChecked_v2_<threshold in cents> label in Gmail.
24
- This code does not send email or transmit mailbox data to third-party services.
25
*/
26
27-
    // Collect messages first, then archive them:
27+
28-
    // this prevents inbox changes from affecting pagination.
28+
29-
    const messageIds = [];
29+
const MAX_MESSAGES_PER_RUN = 100;
30-
    let pageToken;
30+
const MIN_API_INTERVAL_MS = 1000;
31
const MAX_RUN_TIME_MS = 240000;
32-
    do {
32+
const MAX_API_RETRIES = 4;
33-
      const options = {
33+
34-
        q: `in:inbox from:${DUB_SENDER} subject:commission subject:Polymarket`,
34+
35-
        maxResults: 500
35+
  const limitCents = Math.round(LIMIT_DOLLARS * 100);
36-
      };
36+
  if (typeof LIMIT_DOLLARS !== 'number' || !Number.isSafeInteger(limitCents) || limitCents < 0) {
37-
      if (pageToken) options.pageToken = pageToken;
37+
    throw new Error('LIMIT_DOLLARS must be a non-negative dollar amount.');
38
  }
39-
      const page = Gmail.Users.Messages.list('me', options);
39+
40-
      for (const message of page.messages || []) {
40+
41-
        messageIds.push(message.id);
41+
42
43-
      pageToken = page.nextPageToken;
43+
  const deadline = Date.now() + MAX_RUN_TIME_MS;
44-
    } while (pageToken);
44+
  const pause = new Error('Resume remaining work on the next scheduled run.');
45
  let nextCallAt = 0;
46-
    let archived = 0;
46+
  let checked = 0;
47
  let archived = 0;
48-
    for (const id of messageIds) {
48+
49-
      const message = Gmail.Users.Messages.get('me', id, {
49+
  // Pace every API attempt, including retries and label operations.
50-
        format: 'metadata',
50+
  function callGmail(request) {
51-
        metadataHeaders: ['Subject', 'From']
51+
    for (let attempt = 0; attempt <= MAX_API_RETRIES; attempt++) {
52-
      });
52+
      const wait = Math.max(0, nextCallAt - Date.now());
53
      if (Date.now() + wait + 10000 >= deadline) throw pause;
54
      if (wait > 0) Utilities.sleep(wait);
55
      nextCallAt = Date.now() + MIN_API_INTERVAL_MS;
56-
      const headers = message.payload.headers || [];
56+
57
      try {
58
        return request();
59
      } catch (error) {
60-
      // Double-check the sender's exact email address.
60+
        const text = String(error && error.message || error);
61
        const temporary = /quota exceeded|rate.?limit.?exceeded|too many requests|\b429\b|backend.?error|service unavailable|internal error|\b50[0234]\b/i.test(text);
62
        if (!temporary) throw error;
63-
      if (address.trim().toLowerCase() !== DUB_SENDER) continue;
63+
        if (attempt === MAX_API_RETRIES) throw pause;
64
65-
      // Read only the commission amount from the subject,
65+
        const delay = Math.min(32000, 2000 * Math.pow(2, attempt)) +
66-
      // not the purchase amount from the message body.
66+
          Math.floor(Math.random() * 1000);
67-
      const subject = getHeader('subject').trim();
67+
        if (Date.now() + delay + 10000 >= deadline) throw pause;
68-
      const match = subject.match(
68+
        Utilities.sleep(delay);
69-
        /^You['’]ve made a \$([0-9]+(?:,[0-9]{3})*)(?:\.([0-9]{2}))? commission from Polymarket$/i
69+
70-
      );
70+
71
  }
72-
      // Leave messages with unrecognized subjects unchanged.
72+
73-
      if (!match) continue;
73+
  function wasRemoved(error) {
74
    return /\b404\b|not found|requested entity was not found/i.test(
75-
      const cents =
75+
      String(error && error.message || error)
76-
        Number(match[1].replace(/,/g, '')) * 100 +
76+
    );
77-
        Number(match[2] || '0');
77+
78
79-
      if (!Number.isSafeInteger(cents)) continue;
79+
80
    const labelName = 'PolymarketChecked_v2_' + limitCents;
81-
      if (cents < LIMIT_DOLLARS * 100) {
81+
    const labels = callGmail(() => Gmail.Users.Labels.list('me')).labels || [];
82-
        // Removing the INBOX label archives the message.
82+
    let marker = labels.find(label => label.name === labelName);
83-
        Gmail.Users.Messages.modify(
83+
84-
          { removeLabelIds: ['INBOX'] },
84+
    if (!marker) {
85-
          'me',
85+
      marker = callGmail(() => Gmail.Users.Labels.create({
86-
          id
86+
        name: labelName,
87
        labelListVisibility: 'labelHide',
88-
        archived++;
88+
        messageListVisibility: 'hide'
89
      }, 'me'));
90
    }
91
92-
    console.log(`Messages archived: ${archived}`);
92+
    // Read one bounded page before modifying any messages. The marker excludes
93
    // completed work on the next run, so there is no shifting-page pagination.
94
    const page = callGmail(() => Gmail.Users.Messages.list('me', {
95
      q: `in:inbox from:${DUB_SENDER} subject:commission subject:Polymarket -label:${labelName}`,
96
      maxResults: MAX_MESSAGES_PER_RUN
97
    }));
98
99
    for (const item of page.messages || []) {
100
      if (Date.now() + 15000 >= deadline) throw pause;
101
102
      let message;
103
      try {
104
        message = callGmail(() => Gmail.Users.Messages.get('me', item.id, {
105
          format: 'metadata',
106
          metadataHeaders: ['Subject', 'From']
107
        }));
108
      } catch (error) {
109
        // A message may have been deleted since the list request.
110
        if (wasRemoved(error)) continue;
111
        throw error;
112
      }
113
114
      if (!(message.labelIds || []).includes('INBOX')) continue;
115
116
      const headers = (message.payload || {}).headers || [];
117
      const getHeader = name =>
118
        (headers.find(h => h.name.toLowerCase() === name) || {}).value || '';
119
120
      const from = getHeader('from').trim();
121
      const address = (from.match(/<([^<>]+)>/) || [null, from])[1];
122
      let shouldArchive = false;
123
124
      // Require the exact sender and a recognized commission subject.
125
      // Unrecognized emails stay in the inbox and receive only the marker.
126
      if (address.trim().toLowerCase() === DUB_SENDER) {
127
        const match = getHeader('subject').trim().match(
128
          /^You['’]ve made a \$([0-9]+(?:,[0-9]{3})*)(?:\.([0-9]{2}))? commission from Polymarket$/i
129
        );
130
131
        if (match) {
132
          const cents = Number(match[1].replace(/,/g, '')) * 100 +
133
            Number(match[2] || '0');
134
          shouldArchive = Number.isSafeInteger(cents) && cents < limitCents;
135
        }
136
      }
137
138
      const changes = { addLabelIds: [marker.id] };
139
      if (shouldArchive) changes.removeLabelIds = ['INBOX'];
140
141
      try {
142
        // Archive and record completion in the same idempotent operation.
143
        callGmail(() => Gmail.Users.Messages.modify(changes, 'me', item.id));
144
      } catch (error) {
145
        if (wasRemoved(error)) continue;
146
        throw error;
147
      }
148
149
      checked++;
150
      if (shouldArchive) archived++;
151
    }
152
  } catch (error) {
153
    if (error !== pause) throw error;
154
    console.warn('Run paused. Remaining messages will be retried on the next scheduled run.');
155
  } finally {
156
    console.log(`Messages checked: ${checked}. Messages archived: ${archived}.`);
157
    lock.releaseLock();
158
  }
159
}
160