I wish someone had just posted code like this when I first learned animation, some 8-10 years ago. (Has it been that long?)
Assuming OpenGL conventions (column vertices on the right, matrices multiply from right to left in operation order).
Note: The data needed for skinned animation includes the actual mesh with bone indices/weights, as well as the animation data with bone position/rotation per frame. Typically, you'll get this from a DCC application like 3ds Max, Maya, or perhaps MilkShape.
```// Character soft skinning with animation blending, by Jon Watte.
// Free software under the MIT license.
// Assumes some existing utility/math functions and data types.
// Vector3: x,y,z
// Quaternion: x,y,z,w
// Matrix: 16 elements, translation on right, column major
// Matrix::translation(), Matrix::rotation() and normalize(Quaternion)

// definitions

struct Vertex {
Vector3 pos; // in object space
Vector3 normal; // should be normalized
Vector2 texCoord;
unsigned byte indices; // < numBones
float weights; // should sum up to 1
};

struct Bone {  // assuming bone positions relative to identity pose in world space
Vector3 offset;
Quaternion ori;
};

Vertex *iVertices, *oVertices;
int numVertices;
Matrix *boneMatrix;
Bone **animationFrames;
int numBones;
GLuint buffer;

// set-up
// load iVertices and animationFrames, numVertices and numBones
...
// allocate oVertices and boneMatrix
...
// allocate the vertex buffer object
glGenBuffers(1, &buffer);
glBindBuffer(GL_ELEMENT_ARRAY, buffer);
glBufferData(GL_ELEMENT_ARRAY, 0, 0, GL_DYNAMIC_DRAW);

// to render, based on frames A and B

// derive the pose from keyframe A and B (lerp by factor L)
for (int i = 0; i < numBones; ++i) {
boneMatrix[i] =
Matrix::translation(animationFrames[A][i].offset * (1-L)
+ animationFrames[B][i].offset * L)
*
Matrix::rotation(normalize(
animationFrames[A][i].ori * (1-L)
+ animationFrames[B][i].ori * L));
}

// calculate the vertex position and normal based on 4-bone blending
for (int i = 0; i < numVertices; ++i) {
oVertices[i] = iVertices[i];
oVertices[i].pos =
iVertices[i].pos * boneMatrix[iVertices[i].indices] * iVertices[i].weights
+ iVertices[i].pos * boneMatrix[iVertices[i].indices] * iVertices[i].weights
+ iVertices[i].pos * boneMatrix[iVertices[i].indices] * iVertices[i].weights
+ iVertices[i].pos * boneMatrix[iVertices[i].indices] * iVertices[i].weights;
// this only works if there is no non-uniform scale in the bones
oVertices[i].normal =
iVertices[i].normal * boneMatrix[iVertices[i].indices] * iVertices[i].weights
+ iVertices[i].normal * boneMatrix[iVertices[i].indices] * iVertices[i].weights
+ iVertices[i].normal * boneMatrix[iVertices[i].indices] * iVertices[i].weights
+ iVertices[i].normal * boneMatrix[iVertices[i].indices] * iVertices[i].weights;
}

glBindBuffer(GL_ELEMENT_ARRAY, buffer);
glBufferSubData(GL_ELEMENT_ARRAY, 0, sizeof(oVertices)*numVertices, oVertices);
glVertexPointer(3, GL_FLOAT, sizeof(oVertices), 0);
glNormalPointer(GL_FLOAT, sizeof(oVertices), 12);
glTexCoordPointer(2, GL_FLOAT, sizeof(oVertices), 24);
glDrawRangeElements( /* whatever your triangle list is */ );
```