Skip to main content
Ghost credentials allow you to create identity records and assign points to users before they’ve connected to your realm. This is powerful for pre-seeding rewards, running off-platform campaigns, or migrating users from external systems.

Overview

A ghost credential is a non-verified identity record (Twitter ID, wallet address, email, etc.) that can accumulate point balances independently. When the user eventually connects that identity to your realm, the ghost credential is linked to their account and all accumulated balances are transferred.
Ghost credentials are realm-specific. The same Twitter ID can exist as separate ghost credentials in different realms, each with their own point balances.

Use Cases

Pre-Credit Campaigns

Award points to Twitter followers or wallet holders before they join your community

Off-Platform Activity

Track and reward activity that happens outside your realm (external events, partnerships)

User Migration

Import users from external systems with their pre-existing point balances

Email Onboarding

Assign welcome bonuses to email addresses before users complete signup

Credential Types

Ghost credentials support multiple identity formats:
TypeDescriptionExample Value
twitter-idTwitter/X user ID1234567890
discord-idDiscord user ID123456789012345678
walletBlockchain wallet address0x742d35Cc6634C0532925a3b844Bc454e4438f44e
emailEmail address[email protected]
customCustom identifier (requires source)customer_12345

Creating Credentials

Social Credentials

Create credentials for social platform identities like Twitter or Discord:
async function createTwitterCredential(realmId, twitterId, username) {
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/social`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        provider: 'twitter',
        providerId: twitterId,
        username: username
      })
    }
  );
  
  return response.json();
}

// Create a ghost credential for a Twitter user
const credential = await createTwitterCredential(
  'YOUR_REALM_ID',
  '1234567890',
  'twitter_handle'
);

Wallet Credentials

Create credentials for blockchain wallet addresses:
async function createWalletCredential(realmId, address, chain) {
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/wallet`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        address: address,
        chain: chain
      })
    }
  );
  
  return response.json();
}

// Create a ghost credential for an Ethereum wallet
const credential = await createWalletCredential(
  'YOUR_REALM_ID',
  '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
  'ethereum'
);

Finding Credentials

Look up existing ghost credentials by their identifier:
async function findCredential(realmId, type, value) {
  const params = new URLSearchParams({ type, value });
  
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/find?${params}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`
      }
    }
  );
  
  if (response.status === 404) {
    return null; // Credential doesn't exist
  }
  
  return response.json();
}

// Find by Twitter ID
const twitterCred = await findCredential('YOUR_REALM_ID', 'twitter-id', '1234567890');

// Find by wallet address
const walletCred = await findCredential('YOUR_REALM_ID', 'wallet', '0x742d35Cc...');

// Find by email
const emailCred = await findCredential('YOUR_REALM_ID', 'email', '[email protected]');

Managing Balances

Update Balance

Award or deduct points from a ghost credential:
async function updateCredentialBalance(realmId, type, value, amount, realmPointId = null) {
  const params = new URLSearchParams({ type, value });
  
  const body = { amount };
  if (realmPointId) {
    body.realmPointId = realmPointId;
  }
  
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/balance?${params}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    }
  );
  
  return response.json();
}

// Award 100 points to a Twitter user (uses default currency)
await updateCredentialBalance('YOUR_REALM_ID', 'twitter-id', '1234567890', 100);

// Deduct 50 points from a wallet
await updateCredentialBalance('YOUR_REALM_ID', 'wallet', '0x742d35Cc...', -50);

// Award 200 of a specific currency
await updateCredentialBalance('YOUR_REALM_ID', 'email', '[email protected]', 200, 'CURRENCY_ID');
If realmPointId is not specified, the realm’s default currency is used.

Batch Updates

Update multiple credentials in a single request:
async function batchUpdateBalances(realmId, updates) {
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/transaction`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ updates })
    }
  );
  
  return response.json();
}

// Batch award points to multiple credentials
const result = await batchUpdateBalances('YOUR_REALM_ID', [
  { type: 'twitter-id', value: '1234567890', amount: 100 },
  { type: 'twitter-id', value: '9876543210', amount: 150 },
  { type: 'wallet', value: '0x742d35Cc...', amount: 200 },
  { type: 'email', value: '[email protected]', amount: 75 }
]);

console.log(`Success: ${result.results.length}, Errors: ${result.errors.length}`);

Transfer Points

Transfer points between two credentials:
async function transferPoints(realmId, from, to, amount, realmPointId = null) {
  const params = new URLSearchParams({
    fromType: from.type,
    fromValue: from.value,
    toType: to.type,
    toValue: to.value
  });
  
  const body = { amount };
  if (realmPointId) {
    body.realmPointId = realmPointId;
  }
  
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/transfer?${params}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    }
  );
  
  return response.json();
}

// Transfer 50 points from one Twitter user to another
await transferPoints(
  'YOUR_REALM_ID',
  { type: 'twitter-id', value: '1234567890' },
  { type: 'twitter-id', value: '9876543210' },
  50
);

// Transfer points from a wallet to an email
await transferPoints(
  'YOUR_REALM_ID',
  { type: 'wallet', value: '0x742d35Cc...' },
  { type: 'email', value: '[email protected]' },
  100
);

Linking to Accounts

When a user connects their identity to your realm, you can link the ghost credential to their account. This transfers all accumulated balances to their account.
async function linkCredentialToAccount(realmId, type, value, accountId) {
  const params = new URLSearchParams({ type, value, accountId });
  
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/link?${params}`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`
      }
    }
  );
  
  if (response.status === 204) {
    return { success: true };
  }
  
  return response.json();
}

// Link a Twitter credential to a user's account
await linkCredentialToAccount(
  'YOUR_REALM_ID',
  'twitter-id',
  '1234567890',
  'ACCOUNT_ID'
);
Linking is a one-way operation. Once linked, ghost balances are transferred to the account and the credential cannot be unlinked. The credential will still exist but future balance updates will go directly to the account.

What Happens When Linking

  1. Ghost credential is found by identifier
  2. All GhostCredentialPointBalance records are transferred to AccountPointBalance
  3. Ghost balance records are deleted
  4. Credential gets accountId set
  5. If the account has a verified credential with the same identity, the ghost credential can auto-upgrade to verified

Metadata

Store custom data on credentials for your application’s needs:
async function updateCredentialMetadata(realmId, type, value, metadata) {
  const params = new URLSearchParams({ type, value });
  
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials/metadata?${params}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ metadata })
    }
  );
  
  return response.status === 204;
}

// Store custom metadata
await updateCredentialMetadata(
  'YOUR_REALM_ID',
  'twitter-id',
  '1234567890',
  {
    source: 'airdrop_campaign_2024',
    tier: 'gold',
    referredBy: 'partner_xyz'
  }
);
Metadata is returned when finding credentials and can be used to track campaign attribution, user segments, or any custom data your application needs.

Deleting Credentials

Remove ghost credentials that are no longer needed:
async function deleteCredential(realmId, type, value) {
  const params = new URLSearchParams({ type, value });
  
  const response = await fetch(
    `https://api.drip.re/api/v1/realms/${realmId}/credentials?${params}`,
    {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${process.env.DRIP_API_KEY}`
      }
    }
  );
  
  return response.status === 204;
}

// Delete a ghost credential
await deleteCredential('YOUR_REALM_ID', 'twitter-id', '1234567890');
You cannot delete credentials that have non-zero point balances. Zero out the balance first or link the credential to transfer the balance.

Custom Credential Types

For identifiers that don’t fit the standard types, use the custom type with a source parameter:
// Create a custom credential (e.g., Shopify customer ID)
const response = await fetch(
  `https://api.drip.re/api/v1/realms/${realmId}/credentials/social`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.DRIP_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      provider: 'shopify',  // Your custom source
      providerId: 'customer_12345',
      username: 'Customer Name'
    })
  }
);

// Find by custom type
const credential = await findCredential('YOUR_REALM_ID', 'custom', 'customer_12345');
// Note: For custom types, include source parameter:
// ?type=custom&value=customer_12345&source=shopify

Best Practices

Batch Operations

Use batch endpoints when updating multiple credentials to reduce API calls and improve performance

Idempotent Creates

Creating a credential that already exists returns a 409 Conflict - check first or handle the error

Track Attribution

Use metadata to track where credentials came from (campaigns, partners, events)

Clean Up

Periodically review and delete unused credentials with zero balances

Error Handling

Common errors and how to handle them:
ErrorCauseSolution
409 ConflictCredential already existsFind existing credential instead of creating
404 Not FoundCredential doesn’t existCreate the credential first
400 INSUFFICIENT_BALANCETrying to deduct more than availableCheck balance before deducting
400 CANNOT_DELETE_WITH_BALANCESCredential has non-zero balanceZero out balance or link first

Next Steps