exPricer is a dynamic pricing system for art works that adjusts prices based on exclusivity. Buyers of a work can choose to pay more for more exclusivity, reducing (or even eliminating) any other copies for sale after their purchase.
This project includes both a dynamic pricing algorithm API as well as a ready-to-use web checkout and payment system (powered by Stripe) that uses the same dynamic pricing algorithm. The web checkout system is meant for digital artists who are looking to sell a limited number of a specific work, such as an image, a music file, or any other document, even a ZIP file (which can contain multiple files, such as a music album).
This project was developed by the International Klein Blue token collective. We are a collective of techno-artists inspired by the work of Yves Klein, in particular the questions he asked about how we value art, how art is priced, etc. Our collective is federated by a community token on the Solana blockchain with the symbol IKB.
This project can be freely used by all and is licensed under the Creative Commons Attribution 4.0 International license (CC BY 4.0). When using this project code, you must include attribution to "International Klein Blue (IKB) token collective - ikb-token.co" somewhere on your website.
- A web hosting account or personal web server that has PHP 7.4 or higher
- If you wish to use the web checkout system, you will need to have a Stripe account; it's not necessary if you are interested in just using the exPricer API
If you're an artist looking to sell your digital work with dynamic pricing, follow these simple steps:
-
Download and Upload
- Download all files to your web server
- Make sure your server has PHP 7.4 or higher installed
-
Configure Your Settings
- Make a copy of the sample configuration file
.env.exampleand name the new copy.env - Fill in your settings in this
.envfile (this will include details about the digital file you are selling, the maximum number of copies you are selling, the minimum price, and technical details such as your Stripe account keys and email account). - If you don't have a Stripe account go here: https://dashboard.stripe.com/register, and to learn about where to get your Stripe account keys, refer to: https://docs.stripe.com/keys
- Make a copy of the sample configuration file
-
Set Up Your Files
- Upload all exPricer project files to your web hosting account or personal web server. We recommend putting everything in a new folder, named as you like, or simply
buy. (We decided to keep it simple, just use FTP/sFTP and copy over everything as is, in addition to your .env file of course!) - Upload your digital file to the
downloadsfolder
- Upload all exPricer project files to your web hosting account or personal web server. We recommend putting everything in a new folder, named as you like, or simply
-
Test Your Setup
- Visit the web URL of your web hosting account or web server, remembering to add the folder name you created. For example, this could look something like https://hosting-company.com/myaccount/buy
- Try a test purchase using Stripe's test card (4242 4242 4242 4242, any expiry date in the future, any 3-digit number as card validation code)
- If it's a digital work, verify that the download link works and that the same link is sent by email
- If you would like to adjust the text of the emails sent to buyers, look inside
success.php - After your test purchase(s) you can reset the sales history by deleting the
sales_state.jsonfile located in thedatafolder
Checkout page screenshot
-
Pricing Logic (API + web checkout system)
- Price increases a little bit as fewer copies remain
- The last copy is priced at a premium
- A buyer choosing more exclusivity can make the price jump (as they are effectively paying you not to sell a certain number of copies that would be remaining)
- For the same set of conditions (minimum price, number of copies remaining), a physical work will be priced at a small premium compared to the calculated price if it had been a digital work; this is to compensate the artist with the time to handle shipping or buyer pick-up (notwithstanding any actual shipping costs that the artist may want to pass along later to the buyer for actual shipping).
-
Customer Experience (web checkout system)
- Customer visits your checkout page
- They see different pricing options based on exclusivity
- They select their preferred option and enter their email address
- They complete payment through Stripe
- If it is a digital item, they receive a download link on a payment success page and via email; if it is a physical item, they receive a simple success message and an email requesting their full shipping address
- Keep your
.envfile secure and never share it (it contains sensitive information!) - The
downloadsanddatadirectories are automatically protected from public web access - Test the system thoroughly before going live
If you need help setting up or using exPricer:
- Make sure you've followed all the steps in the Quick Start for Artists section
- Check that your
.envfile is properly configured - Verify that your web hosting account or personal web server has PHP 7.4 or higher installed
- Ensure that your Stripe account is properly set up (e.g. not pending verification, not blocked, etc.)
- If you're still having issues, please open an issue here on GitHub
This section is for those interested in the exPricer dynamic pricing API, for example to integrate exPricer into another web checkout system or a custom web app.
- Clone the repository to your web server
- Ensure PHP 7.4 or higher is installed
- The API is now ready to use
GET /api/v1/health
Checks the health status of the API.
{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2024-03-21T12:00:00+00:00",
"service": "exPricer API"
}POST /api/v1/calculate
Calculates prices for different exclusivity levels based on the input parameters.
{
"work_type": "physical", // or "digital"
"copies_sold": 0, // number of copies already sold
"max_copies": 200, // maximum number of copies allowed
"min_price": 100 // minimum acceptable price in $
}{
"exclusivity_levels": [
{
"remaining_copies": 199, // number of copies that would be left for sale after this purchase
"price": 120,
"percentage_of_edition": 99.5, // percentage of edition that would be left after this purchase
"is_last_copy": false
},
// ... more exclusivity levels ...
],
"current_state": {
"total_copies": 200,
"copies_sold": 0,
"copies_remaining": 200,
"work_type": "physical",
"min_price": 100,
"work_type_factor": 1.2,
"current_market_price": 120
}
}{
"error": "Invalid input",
"message": "Missing required field: work_type"
}{
"error": "Method not allowed. Only POST requests are accepted."
}{
"error": "Internal server error",
"message": "Error details..."
}# Health check
curl http://your-domain/api/v1/health
# Calculate prices
curl -X POST http://your-domain/api/v1/calculate \
-H "Content-Type: application/json" \
-d '{
"work_type": "physical",
"copies_sold": 0,
"max_copies": 100,
"min_price": 100
}'<?php
class ExPricerClient {
private string $baseUrl;
public function __construct(string $baseUrl) {
$this->baseUrl = rtrim($baseUrl, '/');
}
public function checkHealth(): array {
$response = $this->makeRequest('GET', '/api/v1/health');
return json_decode($response, true);
}
public function calculatePrices(
string $workType,
int $copiesSold,
int $maxCopies,
float $minPrice
): array {
$data = [
'work_type' => $workType,
'copies_sold' => $copiesSold,
'max_copies' => $maxCopies,
'min_price' => $minPrice
];
$response = $this->makeRequest('POST', '/api/v1/calculate', $data);
return json_decode($response, true);
}
private function makeRequest(string $method, string $endpoint, ?array $data = null): string {
$ch = curl_init($this->baseUrl . $endpoint);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => ['Content-Type: application/json']
];
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
throw new RuntimeException('API request failed: ' . curl_error($ch));
}
curl_close($ch);
if ($httpCode >= 400) {
$error = json_decode($response, true);
throw new RuntimeException(
$error['message'] ?? 'API request failed with status ' . $httpCode
);
}
return $response;
}
}
// Usage example
try {
$client = new ExPricerClient('http://your-domain');
// Check API health
$health = $client->checkHealth();
echo "API Status: " . $health['status'] . "\n";
// Calculate prices for a physical work
$result = $client->calculatePrices(
workType: 'physical',
copiesSold: 0,
maxCopies: 100,
minPrice: 100
);
// Display current market price
echo "Current market price: $" . $result['current_state']['current_market_price'] . "\n";
// Display all exclusivity levels
foreach ($result['exclusivity_levels'] as $level) {
echo sprintf(
"Remaining copies: %d, Price: $%.2f\n",
$level['remaining_copies'],
$level['price']
);
}
} catch (RuntimeException $e) {
echo "Error: " . $e->getMessage() . "\n";
}import requests
API_BASE_URL = "http://your-domain/api/v1"
# Health check
response = requests.get(f"{API_BASE_URL}/health")
print(response.json())
# Calculate prices
data = {
"work_type": "physical",
"copies_sold": 0,
"max_copies": 100,
"min_price": 100
}
response = requests.post(
f"{API_BASE_URL}/calculate",
json=data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
result = response.json()
print("Current market price:", result["current_state"]["current_market_price"])
for level in result["exclusivity_levels"]:
print(f"Remaining copies: {level['remaining_copies']}, Price: ${level['price']}")
else:
print("Error:", response.json())const fetch = require('node-fetch'); // or use axios
const API_BASE_URL = "http://your-domain/api/v1";
// Health check
async function checkHealth() {
const response = await fetch(`${API_BASE_URL}/health`);
const data = await response.json();
console.log('API Status:', data.status);
}
// Calculate prices
async function calculatePrices() {
const data = {
work_type: "physical",
copies_sold: 0,
max_copies: 100,
min_price: 100
};
try {
const response = await fetch(`${API_BASE_URL}/calculate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
console.log('Current market price:', result.current_state.current_market_price);
result.exclusivity_levels.forEach(level => {
console.log(`Remaining copies: ${level.remaining_copies}, Price: $${level.price}`);
});
} else {
console.error('Error:', result.error);
}
} catch (error) {
console.error('Request failed:', error);
}
}
// Run examples
checkHealth();
calculatePrices();The pricing system works as follows:
- Each work has a minimum price (
min_price) - Physical works have a 20% premium over digital works (work type factor)
- The current market price increases as more copies are sold (market factor)
- Market factor = 1 + (copies_sold / max_copies) * 0.5
The market factor creates a dynamic pricing model that reflects the increasing value of remaining copies:
- Starting Point: When no copies are sold, the market factor is 1.0 (no increase)
- Linear Growth: The factor increases proportionally as copies are sold
- Maximum Increase: The factor can reach up to 1.5 (50% increase) when all but one copy is sold
- Formula: Market Factor = 1 + (copies_sold / max_copies) * 0.5
Example for a 10-copy edition with $100 minimum price:
- First copy: $100 (market factor: 1.0)
- Fifth copy: $125 (market factor: 1.25)
- Last copy: $145 (market factor: 1.45)
This creates a natural price progression that:
- Rewards early buyers with lower prices
- Reflects increasing scarcity as the edition sells
- Maintains fair value throughout the edition
- Provides predictable price increases
- The system automatically calculates appropriate exclusivity levels based on the total number of copies made available for sale
- For small editions (≤10 copies), more granular options are offered to the purchaser
- For larger editions, percentage-based reductions in remaining supply (i.e. more optional paid-for exclusivity) are offered to the purchaser
- The purchaser always has the option to force their purchased copy to be the last copy sold.
- The price for each exclusivity level is based on:
- The current market price for one copy
- Plus the value of all to-be-eliminated copies at the current market price
- Current market price: $120 (base price × 1.2 for physical)
- Last copy price: $12,000 (current market price + value of 99 eliminated copies)
- Current market price: $125 (base price × market factor 1.25)
- Last copy price: $6,250 (current market price + value of 49 eliminated copies)
- Item initial price: $100
- More granular options (1, 2, 3 copies)
- Last copy price: $1,000 (current market price + value of 9 eliminated copies)
Contributions are welcome! Please feel free to submit a pull request. When contributing, please ensure you maintain the attribution requirements of the license.
