Export your Coinbase transaction history to CSV or JSON format
Size
16.8 KB
Version
1.0.0
Created
Nov 27, 2025
Updated
15 days ago
1// ==UserScript==
2// @name Coinbase Transaction Exporter
3// @description Export your Coinbase transaction history to CSV or JSON format
4// @version 1.0.0
5// @match https://*.coinbase.com/*
6// @icon https://www.coinbase.com/img/favicon/favicon-32.png
7// @grant GM.setClipboard
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('Coinbase Transaction Exporter loaded');
13
14 // Utility function to debounce
15 function debounce(func, wait) {
16 let timeout;
17 return function executedFunction(...args) {
18 const later = () => {
19 clearTimeout(timeout);
20 func(...args);
21 };
22 clearTimeout(timeout);
23 timeout = setTimeout(later, wait);
24 };
25 }
26
27 // Extract transaction data from the page
28 function extractTransactionData() {
29 console.log('Extracting transaction data...');
30 const transactions = [];
31
32 // Find all transaction rows
33 const transactionRows = document.querySelectorAll('tr[data-testid="transaction-history-row"]');
34 console.log(`Found ${transactionRows.length} transaction rows`);
35
36 transactionRows.forEach((row, index) => {
37 try {
38 const ariaLabel = row.getAttribute('aria-label') || '';
39
40 // Extract transaction type and amount from aria-label
41 // Format: "Sent USDC -5.07 USDC" or "Received BTC +0.001 BTC"
42 const labelParts = ariaLabel.split(' ');
43
44 // Get all cells in the row
45 const cells = row.querySelectorAll('td');
46
47 // Extract transaction details
48 const transaction = {
49 type: '',
50 asset: '',
51 amount: '',
52 value: '',
53 date: '',
54 status: ''
55 };
56
57 // Parse aria-label for type and amount
58 if (labelParts.length >= 3) {
59 transaction.type = labelParts[0]; // "Sent" or "Received"
60 transaction.asset = labelParts[1]; // "USDC", "BTC", etc.
61 transaction.amount = labelParts.slice(2).join(' '); // "-5.07 USDC"
62 }
63
64 // Extract additional details from cells
65 cells.forEach((cell, cellIndex) => {
66 const cellText = cell.textContent.trim();
67
68 // First cell usually contains type and asset info
69 if (cellIndex === 0) {
70 const typeElement = cell.querySelector('p[class*="headline"]');
71 if (typeElement) {
72 transaction.type = typeElement.textContent.trim();
73 }
74 }
75
76 // Look for value (currency amount like €5.07)
77 if (cellText.match(/[€$£¥]\d+/)) {
78 transaction.value = cellText;
79 }
80
81 // Look for date patterns
82 if (cellText.match(/\d{1,2}\/\d{1,2}\/\d{2,4}/) ||
83 cellText.match(/Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec/)) {
84 transaction.date = cellText;
85 }
86 });
87
88 // Only add if we have meaningful data
89 if (transaction.type || transaction.amount) {
90 transactions.push(transaction);
91 console.log(`Transaction ${index + 1}:`, transaction);
92 }
93 } catch (error) {
94 console.error(`Error extracting transaction ${index}:`, error);
95 }
96 });
97
98 return transactions;
99 }
100
101 // Convert transactions to CSV format
102 function convertToCSV(transactions) {
103 if (transactions.length === 0) {
104 return 'No transactions found';
105 }
106
107 // Get all unique keys from transactions
108 const headers = ['Type', 'Asset', 'Amount', 'Value', 'Date', 'Status'];
109
110 // Create CSV header
111 let csv = headers.join(',') + '\n';
112
113 // Add data rows
114 transactions.forEach(transaction => {
115 const row = [
116 escapeCSV(transaction.type || ''),
117 escapeCSV(transaction.asset || ''),
118 escapeCSV(transaction.amount || ''),
119 escapeCSV(transaction.value || ''),
120 escapeCSV(transaction.date || ''),
121 escapeCSV(transaction.status || '')
122 ];
123 csv += row.join(',') + '\n';
124 });
125
126 return csv;
127 }
128
129 // Escape CSV values
130 function escapeCSV(value) {
131 if (typeof value !== 'string') {
132 value = String(value);
133 }
134 // Escape quotes and wrap in quotes if contains comma, quote, or newline
135 if (value.includes(',') || value.includes('"') || value.includes('\n')) {
136 return '"' + value.replace(/"/g, '""') + '"';
137 }
138 return value;
139 }
140
141 // Download file
142 function downloadFile(content, filename, mimeType) {
143 const blob = new Blob([content], { type: mimeType });
144 const url = URL.createObjectURL(blob);
145 const link = document.createElement('a');
146 link.href = url;
147 link.download = filename;
148 document.body.appendChild(link);
149 link.click();
150 document.body.removeChild(link);
151 URL.revokeObjectURL(url);
152 console.log(`Downloaded ${filename}`);
153 }
154
155 // Export to CSV
156 async function exportToCSV() {
157 console.log('Exporting to CSV...');
158 const transactions = extractTransactionData();
159
160 if (transactions.length === 0) {
161 alert('No transactions found on this page. Please make sure you are on the transactions page.');
162 return;
163 }
164
165 const csv = convertToCSV(transactions);
166 const timestamp = new Date().toISOString().split('T')[0];
167 downloadFile(csv, `coinbase-transactions-${timestamp}.csv`, 'text/csv');
168
169 // Show success message
170 showNotification(`Exported ${transactions.length} transactions to CSV`, 'success');
171 }
172
173 // Export to JSON
174 async function exportToJSON() {
175 console.log('Exporting to JSON...');
176 const transactions = extractTransactionData();
177
178 if (transactions.length === 0) {
179 alert('No transactions found on this page. Please make sure you are on the transactions page.');
180 return;
181 }
182
183 const json = JSON.stringify(transactions, null, 2);
184 const timestamp = new Date().toISOString().split('T')[0];
185 downloadFile(json, `coinbase-transactions-${timestamp}.json`, 'application/json');
186
187 // Show success message
188 showNotification(`Exported ${transactions.length} transactions to JSON`, 'success');
189 }
190
191 // Copy to clipboard
192 async function copyToClipboard() {
193 console.log('Copying to clipboard...');
194 const transactions = extractTransactionData();
195
196 if (transactions.length === 0) {
197 alert('No transactions found on this page. Please make sure you are on the transactions page.');
198 return;
199 }
200
201 const csv = convertToCSV(transactions);
202
203 try {
204 await GM.setClipboard(csv);
205 showNotification(`Copied ${transactions.length} transactions to clipboard`, 'success');
206 } catch (error) {
207 console.error('Error copying to clipboard:', error);
208 showNotification('Failed to copy to clipboard', 'error');
209 }
210 }
211
212 // Show notification
213 function showNotification(message, type = 'info') {
214 const notification = document.createElement('div');
215 notification.textContent = message;
216 notification.style.cssText = `
217 position: fixed;
218 top: 20px;
219 right: 20px;
220 background: ${type === 'success' ? '#0052FF' : type === 'error' ? '#FF4444' : '#333'};
221 color: white;
222 padding: 16px 24px;
223 border-radius: 8px;
224 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
225 font-size: 14px;
226 font-weight: 500;
227 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
228 z-index: 999999;
229 animation: slideIn 0.3s ease-out;
230 `;
231
232 document.body.appendChild(notification);
233
234 setTimeout(() => {
235 notification.style.animation = 'slideOut 0.3s ease-out';
236 setTimeout(() => {
237 if (notification.parentNode) {
238 notification.parentNode.removeChild(notification);
239 }
240 }, 300);
241 }, 3000);
242 }
243
244 // Add CSS animations
245 TM_addStyle(`
246 @keyframes slideIn {
247 from {
248 transform: translateX(400px);
249 opacity: 0;
250 }
251 to {
252 transform: translateX(0);
253 opacity: 1;
254 }
255 }
256
257 @keyframes slideOut {
258 from {
259 transform: translateX(0);
260 opacity: 1;
261 }
262 to {
263 transform: translateX(400px);
264 opacity: 0;
265 }
266 }
267
268 .coinbase-export-button {
269 display: flex;
270 align-items: center;
271 justify-content: center;
272 gap: 8px;
273 background: #0052FF;
274 color: white;
275 border: none;
276 border-radius: 8px;
277 padding: 12px 20px;
278 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
279 font-size: 14px;
280 font-weight: 600;
281 cursor: pointer;
282 transition: all 0.2s ease;
283 box-shadow: 0 2px 4px rgba(0, 82, 255, 0.2);
284 }
285
286 .coinbase-export-button:hover {
287 background: #0045DD;
288 box-shadow: 0 4px 8px rgba(0, 82, 255, 0.3);
289 transform: translateY(-1px);
290 }
291
292 .coinbase-export-button:active {
293 transform: translateY(0);
294 box-shadow: 0 2px 4px rgba(0, 82, 255, 0.2);
295 }
296
297 .coinbase-export-dropdown {
298 position: absolute;
299 top: 100%;
300 right: 0;
301 margin-top: 8px;
302 background: white;
303 border-radius: 8px;
304 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
305 overflow: hidden;
306 z-index: 999999;
307 min-width: 200px;
308 }
309
310 .coinbase-export-dropdown-item {
311 display: flex;
312 align-items: center;
313 gap: 12px;
314 padding: 12px 16px;
315 background: white;
316 color: #050F19;
317 border: none;
318 width: 100%;
319 text-align: left;
320 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
321 font-size: 14px;
322 font-weight: 500;
323 cursor: pointer;
324 transition: background 0.2s ease;
325 }
326
327 .coinbase-export-dropdown-item:hover {
328 background: #F5F8FF;
329 }
330
331 .coinbase-export-container {
332 position: relative;
333 display: inline-block;
334 }
335 `);
336
337 // Create export button
338 function createExportButton() {
339 console.log('Creating export button...');
340
341 // Check if button already exists
342 if (document.getElementById('coinbase-export-container')) {
343 console.log('Export button already exists');
344 return;
345 }
346
347 // Find the filter section to place the button
348 const filterSection = document.querySelector('div[class*="sticky"][class*="zIndex"]');
349
350 if (!filterSection) {
351 console.log('Filter section not found, will retry...');
352 return;
353 }
354
355 // Create container
356 const container = document.createElement('div');
357 container.id = 'coinbase-export-container';
358 container.className = 'coinbase-export-container';
359 container.style.cssText = 'margin-left: auto; padding: 0 16px;';
360
361 // Create main button
362 const button = document.createElement('button');
363 button.className = 'coinbase-export-button';
364 button.innerHTML = `
365 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
366 <path d="M8 11L4 7H7V2H9V7H12L8 11Z" fill="currentColor"/>
367 <path d="M2 13H14V15H2V13Z" fill="currentColor"/>
368 </svg>
369 Export
370 `;
371
372 // Create dropdown
373 const dropdown = document.createElement('div');
374 dropdown.className = 'coinbase-export-dropdown';
375 dropdown.style.display = 'none';
376
377 // CSV option
378 const csvOption = document.createElement('button');
379 csvOption.className = 'coinbase-export-dropdown-item';
380 csvOption.innerHTML = `
381 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
382 <path d="M3 2H13V14H3V2Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
383 <path d="M5 5H11M5 8H11M5 11H9" stroke="currentColor" stroke-width="1.5"/>
384 </svg>
385 Export as CSV
386 `;
387 csvOption.addEventListener('click', () => {
388 dropdown.style.display = 'none';
389 exportToCSV();
390 });
391
392 // JSON option
393 const jsonOption = document.createElement('button');
394 jsonOption.className = 'coinbase-export-dropdown-item';
395 jsonOption.innerHTML = `
396 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
397 <path d="M4 2H12V14H4V2Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
398 <path d="M6 5H10M6 8H10M6 11H8" stroke="currentColor" stroke-width="1.5"/>
399 </svg>
400 Export as JSON
401 `;
402 jsonOption.addEventListener('click', () => {
403 dropdown.style.display = 'none';
404 exportToJSON();
405 });
406
407 // Copy option
408 const copyOption = document.createElement('button');
409 copyOption.className = 'coinbase-export-dropdown-item';
410 copyOption.innerHTML = `
411 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
412 <path d="M5 5V3H13V11H11" stroke="currentColor" stroke-width="1.5" fill="none"/>
413 <path d="M3 5H11V13H3V5Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
414 </svg>
415 Copy to Clipboard
416 `;
417 copyOption.addEventListener('click', () => {
418 dropdown.style.display = 'none';
419 copyToClipboard();
420 });
421
422 dropdown.appendChild(csvOption);
423 dropdown.appendChild(jsonOption);
424 dropdown.appendChild(copyOption);
425
426 // Toggle dropdown
427 button.addEventListener('click', (e) => {
428 e.stopPropagation();
429 dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
430 });
431
432 // Close dropdown when clicking outside
433 document.addEventListener('click', () => {
434 dropdown.style.display = 'none';
435 });
436
437 container.appendChild(button);
438 container.appendChild(dropdown);
439
440 // Find the flex container that holds the filters
441 const flexContainer = filterSection.querySelector('div[class*="flex"][class*="row"]');
442 if (flexContainer) {
443 flexContainer.appendChild(container);
444 console.log('Export button added successfully');
445 } else {
446 console.log('Flex container not found');
447 }
448 }
449
450 // Initialize the extension
451 function init() {
452 console.log('Initializing Coinbase Transaction Exporter...');
453
454 // Check if we're on the transactions page
455 if (window.location.pathname.includes('/transactions')) {
456 // Wait for the page to load
457 setTimeout(() => {
458 createExportButton();
459 }, 2000);
460
461 // Observe DOM changes to re-add button if needed
462 const observer = new MutationObserver(debounce(() => {
463 if (!document.getElementById('coinbase-export-container')) {
464 createExportButton();
465 }
466 }, 1000));
467
468 observer.observe(document.body, {
469 childList: true,
470 subtree: true
471 });
472 }
473 }
474
475 // Run when body is ready
476 TM_runBody(init);
477})();