<div class="vcg-wrap">
<p class="hint">Adjust how much each bidder values each item, then run VCG to see who gets what — and how much they pay.</p>
<div class="bidder-section">
<div class="bidder" id="b0">
<div class="bidder-name">Alice</div>
<label>Values A: <span class="val" id="v0-0">8</span>
<input type="range" min="0" max="15" value="8" oninput="updateVal(0,0,this.value)">
</label>
<label>Values B: <span class="val" id="v0-1">5</span>
<input type="range" min="0" max="15" value="5" oninput="updateVal(0,1,this.value)">
</label>
<label>Values A+B: <span class="val" id="v0-2">12</span>
<input type="range" min="0" max="20" value="12" oninput="updateVal(0,2,this.value)">
</label>
</div>
<div class="bidder" id="b1">
<div class="bidder-name">Bob</div>
<label>Values B: <span class="val" id="v1-1">9</span>
<input type="range" min="0" max="15" value="9" oninput="updateVal(1,1,this.value)">
</label>
<label>Values C: <span class="val" id="v1-2">6</span>
<input type="range" min="0" max="15" value="6" oninput="updateVal(1,2,this.value)">
</label>
<label>Values B+C: <span class="val" id="v1-3">13</span>
<input type="range" min="0" max="20" value="13" oninput="updateVal(1,3,this.value)">
</label>
</div>
<div class="bidder" id="b2">
<div class="bidder-name">Carol</div>
<label>Values A: <span class="val" id="v2-0">6</span>
<input type="range" min="0" max="15" value="6" oninput="updateVal(2,0,this.value)">
</label>
<label>Values C: <span class="val" id="v2-2">10</span>
<input type="range" min="0" max="15" value="10" oninput="updateVal(2,2,this.value)">
</label>
<label>Values A+C: <span class="val" id="v2-3">14</span>
<input type="range" min="0" max="20" value="14" oninput="updateVal(2,3,this.value)">
</label>
</div>
</div>
<button id="run" onclick="runVCG()">Run VCG</button>
<div id="result" class="result hidden"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.vcg-wrap { padding: 0 2px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.4; }
.bidder-section { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: .8rem; }
.bidder { flex: 1; min-width: 150px; background: #f0f4f8; border: 1px solid #cdd9e5;
border-radius: 10px; padding: .7rem .8rem; }
.bidder-name { font-weight: 700; font-size: .95rem; color: #1d3557; margin-bottom: .5rem; }
label { display: block; margin-bottom: .45rem; color: #333; font-size: .82rem; line-height: 1.3; }
.val { font-weight: 700; color: #1d3557; min-width: 18px; display: inline-block; }
input[type=range] { display: block; width: 100%; margin-top: 2px; accent-color: #1d3557; }
button { font: 600 14px system-ui; padding: .45rem 1.1rem; background: #1d3557;
color: #fff; border: none; border-radius: 8px; cursor: pointer; margin-bottom: .8rem; }
button:hover { background: #16304d; }
.result { background: #eaf4ea; border: 1px solid #b2d8b2; border-radius: 10px; padding: .8rem 1rem; }
.result.hidden { display: none; }
.result h3 { margin: 0 0 .5rem; font-size: .95rem; color: #0a5c2a; }
.alloc-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: .5rem; }
.alloc-chip { background: #1d3557; color: #fff; border-radius: 6px;
padding: .25rem .55rem; font-size: .8rem; font-weight: 600; }
.payment-table { width: 100%; border-collapse: collapse; font-size: .82rem; margin-top: .4rem; }
.payment-table th { text-align: left; color: #0a5c2a; padding: 2px 6px; border-bottom: 1px solid #b2d8b2; }
.payment-table td { padding: 3px 6px; }
.payment-table tr:nth-child(even) td { background: #d8f0d8; }
.welfare-row { margin-top: .5rem; font-size: .85rem; font-weight: 600; color: #1d3557; }
.expl { font-size: .78rem; color: #555; margin-top: .4rem; line-height: 1.4; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VCG mechanism for 3 bidders and 3 items (A=0, B=1, C=2).
// Each bidder declares values for subsets. VCG:
// 1) Find allocation maximizing total declared value.
// 2) For each agent i, charge them the "externality":
// (welfare of others WITHOUT agent i) - (welfare of others WITH agent i in the optimal allocation).
//
// Bidder values: bids[i] is a map from subset-bitmask to value.
// Items: A=bit0, B=bit1, C=bit2. Subsets: 0b001=A, 0b010=B, 0b100=C, 0b011=A+B, 0b110=B+C, 0b101=A+C, 0b111=A+B+C.
const ITEMS = ['A', 'B', 'C'];
const N_ITEMS = 3;
const N_BIDDERS = 3;
// Initial bid matrices indexed by [bidder][subset_mask]
// Bidder 0 = Alice: values A(001)=8, B(010)=5, A+B(011)=12, others=0
// Bidder 1 = Bob: values B(010)=9, C(100)=6, B+C(110)=13, others=0
// Bidder 2 = Carol: values A(001)=6, C(100)=10, A+C(101)=14, others=0
const bids = [
{ 1:8, 2:5, 3:12 }, // Alice: A=8, B=5, A+B=12
{ 2:9, 4:6, 6:13 }, // Bob: B=9, C=6, B+C=13
{ 1:6, 4:10, 5:14 } // Carol: A=6, C=10, A+C=14
];
// Slider label mapping: bidder -> index in their displayed labels -> subset mask
const SLIDER_MAP = [
[1, 2, 3], // Alice: A, B, A+B
[2, 4, 6], // Bob: B, C, B+C
[1, 4, 5] // Carol: A, C, A+C
];
const SLIDER_LABELS = [
['A', 'B', 'A+B'],
['B', 'C', 'B+C'],
['A', 'C', 'A+C']
];
function updateVal(bidder, idx, val) {
const mask = SLIDER_MAP[bidder][idx];
bids[bidder][mask] = Number(val);
document.getElementById('v' + bidder + '-' + idx).textContent = val;
}
// Get bidder’s value for a subset bitmask
function bidVal(bidder, mask) {
if (mask === 0) return 0;
return bids[bidder][mask] || 0;
}
// Enumerate all feasible allocations (assign disjoint item subsets to bidders).
// Each allocation is an array [mask0, mask1, mask2] where masks are disjoint
// and cover some/all items.
function allAllocations(bidders) {
const n = bidders.length;
const results = [];
// Each item independently assigned to one bidder or unassigned
// Total assignments = (n+1)^3 (including unassigned=n)
const total = Math.pow(n + 1, N_ITEMS);
for (let t = 0; t < total; t++) {
const alloc = new Array(n).fill(0);
let tmp = t;
let valid = true;
for (let item = 0; item < N_ITEMS; item++) {
const owner = tmp % (n + 1);
tmp = Math.floor(tmp / (n + 1));
if (owner < n) {
alloc[owner] |= (1 << item);
}
}
results.push(alloc);
}
return results;
}
// Find optimal allocation for a subset of bidders (by original indices),
// returns { alloc: [mask_per_bidder_in_subset], welfare }
function optimalAlloc(bidderIndices) {
const allocations = allAllocations(bidderIndices);
let bestW = -1, bestAlloc = null;
for (const a of allocations) {
let w = 0;
for (let i = 0; i < bidderIndices.length; i++) {
w += bidVal(bidderIndices[i], a[i]);
}
if (w > bestW) { bestW = w; bestAlloc = a.slice(); }
}
return { alloc: bestAlloc, welfare: bestW };
}
function maskToStr(mask) {
return ITEMS.filter((_, i) => mask & (1 << i)).join('+') || '—';
}
function runVCG() {
// Step 1: find globally optimal allocation
const allBidders = [0, 1, 2];
const { alloc, welfare } = optimalAlloc(allBidders);
// Step 2: for each bidder i, compute externality payment
// externality_i = (optimal welfare of others WITHOUT i) - (welfare of others in current optimal)
const payments = [];
for (let i = 0; i < N_BIDDERS; i++) {
// Others’ welfare in full optimal (excluding agent i’s contribution)
const othersWelfareInOptimal = alloc.reduce((s, m, j) => j !== i ? s + bidVal(j, m) : s, 0);
// Optimal welfare of others WITHOUT agent i
const others = allBidders.filter(x => x !== i);
const { welfare: wOthersAlone } = optimalAlloc(others);
// VCG payment = harm imposed = what others could have had minus what they get
const payment = Math.max(0, wOthersAlone - othersWelfareInOptimal);
payments.push(payment);
}
// Render result
const names = ['Alice', 'Bob', 'Carol'];
const resultEl = document.getElementById('result');
resultEl.classList.remove('hidden');
let allocHTML = '<h3>Efficient Allocation</h3><div class="alloc-row">';
for (let i = 0; i < N_BIDDERS; i++) {
if (alloc[i] > 0) {
allocHTML += `<span class="alloc-chip">${names[i]}: ${maskToStr(alloc[i])}</span>`;
}
}
allocHTML += '</div>';
let tableHTML = '<table class="payment-table"><tr><th>Bidder</th><th>Gets</th><th>True Value</th><th>VCG Payment</th><th>Surplus</th></tr>';
for (let i = 0; i < N_BIDDERS; i++) {
const tv = bidVal(i, alloc[i]);
const surplus = tv - payments[i];
tableHTML += `<tr><td>${names[i]}</td><td>${maskToStr(alloc[i])}</td><td>${tv}</td><td>${payments[i].toFixed(1)}</td><td>${surplus.toFixed(1)}</td></tr>`;
}
tableHTML += '</table>';
const welfareHTML = fmt('<div class="welfare-row">Total welfare: %welfare% | Total payments collected: %reduce%</div>', { welfare: welfare, reduce: payments.reduce((s, p) => s + p, 0).toFixed(1) });
const explHTML = '<p class="expl">Each bidder’s payment = what others would have gained without them minus what others gain with them in this allocation. Bidding your true value is always optimal: overbidding risks overpaying, underbidding risks losing value.</p>';
resultEl.innerHTML = allocHTML + tableHTML + welfareHTML + explHTML;
// Log for verification
console.log('Allocation:', alloc.map((m, i) => names[i] + ':' + maskToStr(m)).join(', '));
console.log('Payments:', payments.map((p, i) => names[i] + ':' + p.toFixed(1)).join(', '));
console.log('Total welfare:', welfare);
}
<div class="vcg-wrap">
<p class="hint">Adjust how much each bidder values each item, then run VCG to see who gets what — and how much they pay.</p>
<div class="bidder-section">
<div class="bidder" id="b0">
<div class="bidder-name">Alice</div>
<label>Values A: <span class="val" id="v0-0">8</span>
<input type="range" min="0" max="15" value="8" oninput="updateVal(0,0,this.value)">
</label>
<label>Values B: <span class="val" id="v0-1">5</span>
<input type="range" min="0" max="15" value="5" oninput="updateVal(0,1,this.value)">
</label>
<label>Values A+B: <span class="val" id="v0-2">12</span>
<input type="range" min="0" max="20" value="12" oninput="updateVal(0,2,this.value)">
</label>
</div>
<div class="bidder" id="b1">
<div class="bidder-name">Bob</div>
<label>Values B: <span class="val" id="v1-1">9</span>
<input type="range" min="0" max="15" value="9" oninput="updateVal(1,1,this.value)">
</label>
<label>Values C: <span class="val" id="v1-2">6</span>
<input type="range" min="0" max="15" value="6" oninput="updateVal(1,2,this.value)">
</label>
<label>Values B+C: <span class="val" id="v1-3">13</span>
<input type="range" min="0" max="20" value="13" oninput="updateVal(1,3,this.value)">
</label>
</div>
<div class="bidder" id="b2">
<div class="bidder-name">Carol</div>
<label>Values A: <span class="val" id="v2-0">6</span>
<input type="range" min="0" max="15" value="6" oninput="updateVal(2,0,this.value)">
</label>
<label>Values C: <span class="val" id="v2-2">10</span>
<input type="range" min="0" max="15" value="10" oninput="updateVal(2,2,this.value)">
</label>
<label>Values A+C: <span class="val" id="v2-3">14</span>
<input type="range" min="0" max="20" value="14" oninput="updateVal(2,3,this.value)">
</label>
</div>
</div>
<button id="run" onclick="runVCG()">Run VCG</button>
<div id="result" class="result hidden"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.vcg-wrap { padding: 0 2px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.4; }
.bidder-section { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: .8rem; }
.bidder { flex: 1; min-width: 150px; background: #f0f4f8; border: 1px solid #cdd9e5;
border-radius: 10px; padding: .7rem .8rem; }
.bidder-name { font-weight: 700; font-size: .95rem; color: #1d3557; margin-bottom: .5rem; }
label { display: block; margin-bottom: .45rem; color: #333; font-size: .82rem; line-height: 1.3; }
.val { font-weight: 700; color: #1d3557; min-width: 18px; display: inline-block; }
input[type=range] { display: block; width: 100%; margin-top: 2px; accent-color: #1d3557; }
button { font: 600 14px system-ui; padding: .45rem 1.1rem; background: #1d3557;
color: #fff; border: none; border-radius: 8px; cursor: pointer; margin-bottom: .8rem; }
button:hover { background: #16304d; }
.result { background: #eaf4ea; border: 1px solid #b2d8b2; border-radius: 10px; padding: .8rem 1rem; }
.result.hidden { display: none; }
.result h3 { margin: 0 0 .5rem; font-size: .95rem; color: #0a5c2a; }
.alloc-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: .5rem; }
.alloc-chip { background: #1d3557; color: #fff; border-radius: 6px;
padding: .25rem .55rem; font-size: .8rem; font-weight: 600; }
.payment-table { width: 100%; border-collapse: collapse; font-size: .82rem; margin-top: .4rem; }
.payment-table th { text-align: left; color: #0a5c2a; padding: 2px 6px; border-bottom: 1px solid #b2d8b2; }
.payment-table td { padding: 3px 6px; }
.payment-table tr:nth-child(even) td { background: #d8f0d8; }
.welfare-row { margin-top: .5rem; font-size: .85rem; font-weight: 600; color: #1d3557; }
.expl { font-size: .78rem; color: #555; margin-top: .4rem; line-height: 1.4; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VCG mechanism for 3 bidders and 3 items (A=0, B=1, C=2).
// Each bidder declares values for subsets. VCG:
// 1) Find allocation maximizing total declared value.
// 2) For each agent i, charge them the "externality":
// (welfare of others WITHOUT agent i) - (welfare of others WITH agent i in the optimal allocation).
//
// Bidder values: bids[i] is a map from subset-bitmask to value.
// Items: A=bit0, B=bit1, C=bit2. Subsets: 0b001=A, 0b010=B, 0b100=C, 0b011=A+B, 0b110=B+C, 0b101=A+C, 0b111=A+B+C.
const ITEMS = ['A', 'B', 'C'];
const N_ITEMS = 3;
const N_BIDDERS = 3;
// Initial bid matrices indexed by [bidder][subset_mask]
// Bidder 0 = Alice: values A(001)=8, B(010)=5, A+B(011)=12, others=0
// Bidder 1 = Bob: values B(010)=9, C(100)=6, B+C(110)=13, others=0
// Bidder 2 = Carol: values A(001)=6, C(100)=10, A+C(101)=14, others=0
const bids = [
{ 1:8, 2:5, 3:12 }, // Alice: A=8, B=5, A+B=12
{ 2:9, 4:6, 6:13 }, // Bob: B=9, C=6, B+C=13
{ 1:6, 4:10, 5:14 } // Carol: A=6, C=10, A+C=14
];
// Slider label mapping: bidder -> index in their displayed labels -> subset mask
const SLIDER_MAP = [
[1, 2, 3], // Alice: A, B, A+B
[2, 4, 6], // Bob: B, C, B+C
[1, 4, 5] // Carol: A, C, A+C
];
const SLIDER_LABELS = [
['A', 'B', 'A+B'],
['B', 'C', 'B+C'],
['A', 'C', 'A+C']
];
function updateVal(bidder, idx, val) {
const mask = SLIDER_MAP[bidder][idx];
bids[bidder][mask] = Number(val);
document.getElementById('v' + bidder + '-' + idx).textContent = val;
}
// Get bidder’s value for a subset bitmask
function bidVal(bidder, mask) {
if (mask === 0) return 0;
return bids[bidder][mask] || 0;
}
// Enumerate all feasible allocations (assign disjoint item subsets to bidders).
// Each allocation is an array [mask0, mask1, mask2] where masks are disjoint
// and cover some/all items.
function allAllocations(bidders) {
const n = bidders.length;
const results = [];
// Each item independently assigned to one bidder or unassigned
// Total assignments = (n+1)^3 (including unassigned=n)
const total = Math.pow(n + 1, N_ITEMS);
for (let t = 0; t < total; t++) {
const alloc = new Array(n).fill(0);
let tmp = t;
let valid = true;
for (let item = 0; item < N_ITEMS; item++) {
const owner = tmp % (n + 1);
tmp = Math.floor(tmp / (n + 1));
if (owner < n) {
alloc[owner] |= (1 << item);
}
}
results.push(alloc);
}
return results;
}
// Find optimal allocation for a subset of bidders (by original indices),
// returns { alloc: [mask_per_bidder_in_subset], welfare }
function optimalAlloc(bidderIndices) {
const allocations = allAllocations(bidderIndices);
let bestW = -1, bestAlloc = null;
for (const a of allocations) {
let w = 0;
for (let i = 0; i < bidderIndices.length; i++) {
w += bidVal(bidderIndices[i], a[i]);
}
if (w > bestW) { bestW = w; bestAlloc = a.slice(); }
}
return { alloc: bestAlloc, welfare: bestW };
}
function maskToStr(mask) {
return ITEMS.filter((_, i) => mask & (1 << i)).join('+') || '—';
}
function runVCG() {
// Step 1: find globally optimal allocation
const allBidders = [0, 1, 2];
const { alloc, welfare } = optimalAlloc(allBidders);
// Step 2: for each bidder i, compute externality payment
// externality_i = (optimal welfare of others WITHOUT i) - (welfare of others in current optimal)
const payments = [];
for (let i = 0; i < N_BIDDERS; i++) {
// Others’ welfare in full optimal (excluding agent i’s contribution)
const othersWelfareInOptimal = alloc.reduce((s, m, j) => j !== i ? s + bidVal(j, m) : s, 0);
// Optimal welfare of others WITHOUT agent i
const others = allBidders.filter(x => x !== i);
const { welfare: wOthersAlone } = optimalAlloc(others);
// VCG payment = harm imposed = what others could have had minus what they get
const payment = Math.max(0, wOthersAlone - othersWelfareInOptimal);
payments.push(payment);
}
// Render result
const names = ['Alice', 'Bob', 'Carol'];
const resultEl = document.getElementById('result');
resultEl.classList.remove('hidden');
let allocHTML = '<h3>Efficient Allocation</h3><div class="alloc-row">';
for (let i = 0; i < N_BIDDERS; i++) {
if (alloc[i] > 0) {
allocHTML += `<span class="alloc-chip">${names[i]}: ${maskToStr(alloc[i])}</span>`;
}
}
allocHTML += '</div>';
let tableHTML = '<table class="payment-table"><tr><th>Bidder</th><th>Gets</th><th>True Value</th><th>VCG Payment</th><th>Surplus</th></tr>';
for (let i = 0; i < N_BIDDERS; i++) {
const tv = bidVal(i, alloc[i]);
const surplus = tv - payments[i];
tableHTML += `<tr><td>${names[i]}</td><td>${maskToStr(alloc[i])}</td><td>${tv}</td><td>${payments[i].toFixed(1)}</td><td>${surplus.toFixed(1)}</td></tr>`;
}
tableHTML += '</table>';
const welfareHTML = fmt('<div class="welfare-row">Total welfare: %welfare% | Total payments collected: %reduce%</div>', { welfare: welfare, reduce: payments.reduce((s, p) => s + p, 0).toFixed(1) });
const explHTML = '<p class="expl">Each bidder’s payment = what others would have gained without them minus what others gain with them in this allocation. Bidding your true value is always optimal: overbidding risks overpaying, underbidding risks losing value.</p>';
resultEl.innerHTML = allocHTML + tableHTML + welfareHTML + explHTML;
// Log for verification
console.log('Allocation:', alloc.map((m, i) => names[i] + ':' + maskToStr(m)).join(', '));
console.log('Payments:', payments.map((p, i) => names[i] + ':' + p.toFixed(1)).join(', '));
console.log('Total welfare:', welfare);
}
Notice how VCG works in two steps. First it finds the allocation that maximizes total declared value. Then it charges each winner the amount the other bidders would have gained if the winner simply hadn't participated — their externality. Because payment equals the harm you impose on others, bidding your true value is always optimal: overbidding risks overpaying, underbidding risks losing an item you value more than the payment would cost.
Comments
Loading comments...